Package me.prettyprint.hom

Source Code of me.prettyprint.hom.HectorObjectMapper

package me.prettyprint.hom;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.Map.Entry;

import javax.persistence.DiscriminatorType;

import me.prettyprint.cassandra.serializers.BooleanSerializer;
import me.prettyprint.cassandra.serializers.BytesArraySerializer;
import me.prettyprint.cassandra.serializers.DateSerializer;
import me.prettyprint.cassandra.serializers.DoubleSerializer;
import me.prettyprint.cassandra.serializers.IntegerSerializer;
import me.prettyprint.cassandra.serializers.LongSerializer;
import me.prettyprint.cassandra.serializers.ObjectSerializer;
import me.prettyprint.cassandra.serializers.StringSerializer;
import me.prettyprint.cassandra.serializers.UUIDSerializer;
import me.prettyprint.hector.api.Keyspace;
import me.prettyprint.hector.api.Serializer;
import me.prettyprint.hector.api.beans.ColumnSlice;
import me.prettyprint.hector.api.beans.HColumn;
import me.prettyprint.hector.api.factory.HFactory;
import me.prettyprint.hector.api.mutation.Mutator;
import me.prettyprint.hector.api.query.QueryResult;
import me.prettyprint.hector.api.query.SliceQuery;
import me.prettyprint.hom.annotations.AnonymousPropertyAddHandler;
import me.prettyprint.hom.annotations.AnonymousPropertyCollectionGetter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Maps a slice of <code>HColumn<String, byte[]></code>s to an object's
* properties. See {@link #createObject(Object, ColumnSlice)} for more details.
* <p/>
* As mentioned above all column names must be <code>String</code>s - doesn't
* really make sense to have other types when mapping to object properties.
*
* @param <T>
*          Type of object mapping to cassandra row
*
* @author Todd Burruss
*/
public class HectorObjectMapper {
  private static Logger logger = LoggerFactory.getLogger(HectorObjectMapper.class);

  private static final int MAX_NUM_COLUMNS = 100;

  private int maxNumColumns = MAX_NUM_COLUMNS;
  private ClassCacheMgr cacheMgr;

  public HectorObjectMapper(ClassCacheMgr cacheMgr) {
    this.cacheMgr = cacheMgr;
  }

  /**
   * Retrieve columns from cassandra keyspace and column family, instantiate a
   * new object of required type, and then map them to the object's properties.
   *
   * @param keyspace
   * @param colFamName
   * @param id
   * @return
   */
  public <T, I> T getObject(Keyspace keyspace, String colFamName, Class<T> clazz, I id) {
    if (null == id) {
      throw new IllegalArgumentException("object ID cannot be null or empty");
    }

    CFMappingDef<T, I> cfMapDef = cacheMgr.getCfMapDef(colFamName, true);
    PropertyMappingDefinition<I> md = cfMapDef.getIdPropertyDef();
    if (null == md) {
      throw new IllegalStateException(
          "Trying to build new object but haven't annotated a field with @Id");
    }

    byte[] idAsBytes = md.getConverter().convertObjTypeToCassType(id);

    SliceQuery<byte[], String, byte[]> q = HFactory.createSliceQuery(keyspace,
        BytesArraySerializer.get(), StringSerializer.get(), BytesArraySerializer.get());
    q.setColumnFamily(colFamName);
    q.setKey(idAsBytes);
    q.setRange("", "", false, maxNumColumns);
    QueryResult<ColumnSlice<String, byte[]>> result = q.execute();
    if (null == result || null == result.get()) {
      return null;
    }

    T obj = createObject(cfMapDef, id, result.get());
    return obj;
  }

  public <T, I> T saveObj(Keyspace keyspace, T obj) {
    if (null == obj) {
      throw new IllegalArgumentException("object cannot be null");
    }

    @SuppressWarnings("unchecked")
    CFMappingDef<T, I> cfMapDef = (CFMappingDef<T, I>) cacheMgr.getCfMapDef(obj.getClass(), true);
    PropertyMappingDefinition<I> md = cfMapDef.getIdPropertyDef();
    if (null == md) {
      throw new IllegalStateException(
          "Trying to save object but haven't annotated a field with @Id");
    }

    Method meth = md.getPropDesc().getReadMethod();
    if (null == meth) {
      logger.debug("@Id annotation found - but can't find getter for property, "
          + md.getPropDesc().getName());
    }

    byte[] bytes;
    try {
      @SuppressWarnings("unchecked")
      I retVal = (I) meth.invoke(obj, (Object[]) null);
      bytes = md.getConverter().convertObjTypeToCassType(retVal);
    } catch (IllegalArgumentException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    } catch (InvocationTargetException e) {
      throw new RuntimeException(e);
    }

    if (null == bytes) {
      throw new IllegalArgumentException("object ID cannot be null or empty");
    }

    Collection<HColumn<String, byte[]>> colColl = createColumnSet(obj);

    String colFamName = cfMapDef.getColFamName();
    Mutator<byte[]> m = HFactory.createMutator(keyspace, BytesArraySerializer.get());
    for (HColumn<String, byte[]> col : colColl) {
      m.addInsertion(bytes, colFamName, col);
    }

    m.execute();

    return obj;
  }

  /**
   * Given a column slice from Hector/Cassandra and a type, this method
   * instantiates the object and sets the properties on the object using the
   * slice data. If a column doesn't map to an specific property in the object,
   * it will see if the object has implemented the interface,
   * {@link HectorExtraProperties}. If so call
   * {@link HectorExtraProperties#addExtraProperty(String, String)}, on the
   * object.
   *
   * @param id
   *          ID (row key) of the object we are retrieving from Cassandra
   * @param clazz
   *          type of object to instantiate and populate
   * @param slice
   *          column slice from Hector of type
   *          <code>ColumnSlice<String, byte[]></code>
   *
   * @return instantiated object if success, null if slice is empty,
   *         RuntimeException otherwise
   */
  <T, I> T createObject(CFMappingDef<T, I> cfMapDef, I id, ColumnSlice<String, byte[]> slice) {
    if (slice.getColumns().isEmpty()) {
      return null;
    }

    CFMappingDef<? extends T, I> cfMapDefInstance = determineClassType(cfMapDef, slice);

    try {
      T obj = cfMapDefInstance.getClazz().newInstance();

      setIdIfCan(cfMapDef, obj, id);

      for (HColumn<String, byte[]> col : slice.getColumns()) {
        String colName = col.getName();
        PropertyMappingDefinition<?> md = cfMapDefInstance.getPropMapByColumnName(colName);
        if (null != md && null != md.getPropDesc()) {
          setPropertyUsingColumn(obj, col, md);
        }
        // if this is a derived class then don't need to save disc
        // column value
        else if (null != cfMapDef.getDiscColumn() && colName.equals(cfMapDef.getDiscColumn())) {
          continue;
        } else {
          addToExtraIfCan(obj, col);
        }
      }

      return obj;
    } catch (InstantiationException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    } catch (IllegalArgumentException e) {
      throw new RuntimeException(e);
    } catch (InvocationTargetException e) {
      throw new RuntimeException(e);
    } catch (SecurityException e) {
      throw new RuntimeException(e);
    } catch (NoSuchMethodException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Create Set of HColumns for the given Object. The Object must be annotated
   * with {@link Column} on the desired fields.
   *
   * @param obj
   * @return
   */
  private Collection<HColumn<String, byte[]>> createColumnSet(Object obj) {
    Map<String, HColumn<String, byte[]>> map = createColumnMap(obj);
    if (null != map) {
      return map.values();
    } else {
      return null;
    }
  }

  /**
   * Creates a Map of property names as key and HColumns as value. See #
   *
   * @param obj
   * @return
   */
  <T, I> Map<String, HColumn<String, byte[]>> createColumnMap(T obj) {
    if (null == obj) {
      throw new IllegalArgumentException("Class type cannot be null");
    }

    @SuppressWarnings("unchecked")
    CFMappingDef<T, I> cfMapDef = (CFMappingDef<T, I>) cacheMgr.getCfMapDef(
        (Class<T>) obj.getClass(), true);
    try {
      Map<String, HColumn<String, byte[]>> colSet = new HashMap<String, HColumn<String, byte[]>>();
      Collection<PropertyMappingDefinition<?>> coll = cfMapDef.getAllProperties();
      for (PropertyMappingDefinition<?> md : coll) {
        HColumn<String, byte[]> col = createColumnFromProperty(obj, md);
        if (null != col) {
          colSet.put(col.getName(), col);
        }
      }

      if (null != cfMapDef.getCfBaseMapDef()) {
        CFMappingDef<?, I> cfSuperMapDef = cfMapDef.getCfBaseMapDef();
        String discColName = cfSuperMapDef.getDiscColumn();
        DiscriminatorType discType = cfSuperMapDef.getDiscType();

        colSet.put(discColName, createHColumn(discColName, convertDiscTypeToColValue(discType,
            cfMapDef.getDiscValue())));
      }

      addAnonymousProperties(obj, colSet);
      return colSet;
    } catch (SecurityException e) {
      throw new RuntimeException(e);
    } catch (NoSuchFieldException e) {
      throw new RuntimeException(e);
    } catch (IllegalArgumentException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    } catch (InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  private void addAnonymousProperties(Object obj, Map<String, HColumn<String, byte[]>> colSet)
      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
    Method meth = findAnnotatedMethod(obj.getClass(), AnonymousPropertyCollectionGetter.class);
    if (null == meth) {
      return;
    }

    @SuppressWarnings("unchecked")
    Collection<Entry<String, String>> propColl = (Collection<Entry<String, String>>) meth.invoke(
        obj, (Object[]) null);
    if (null == propColl || propColl.isEmpty()) {
      return;
    }

    for (Entry<String, String> entry : propColl) {
      colSet.put(entry.getKey(), HFactory.createColumn(entry.getKey(), entry.getValue().getBytes(),
          StringSerializer.get(), BytesArraySerializer.get()));
    }
  }

  private <T, P> HColumn<String, byte[]> createColumnFromProperty(T obj,
      PropertyMappingDefinition<P> md) throws SecurityException, NoSuchFieldException,
      IllegalArgumentException, IllegalAccessException, InvocationTargetException {
    byte[] colValue = createBytesFromPropertyValue(obj, md);
    if (null == colValue) {
      return null;
    }
    HColumn<String, byte[]> col = createHColumn(md.getColName(), colValue);
    return col;
  }

  private <P> byte[] createBytesFromPropertyValue(Object obj, PropertyMappingDefinition<P> md)
      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
    PropertyDescriptor pd = md.getPropDesc();
    Method getter = pd.getReadMethod();
    if (null == getter) {
      throw new RuntimeException("missing getter method for property, " + pd.getName());
    }

    @SuppressWarnings("unchecked")
    P retVal = (P) getter.invoke(obj, (Object[]) null);

    // if no value, then signal with null bytes
    if (null == retVal) {
      return null;
    }

    byte[] bytes = md.getConverter().convertObjTypeToCassType(retVal);
    return bytes;
  }

  private HColumn<String, byte[]> createHColumn(String name, byte[] value) {
    return HFactory.createColumn(name, value, StringSerializer.get(), BytesArraySerializer.get());
  }

  private <T, I> CFMappingDef<? extends T, I> determineClassType(CFMappingDef<T, I> cfMapDef,
      ColumnSlice<String, byte[]> slice) {
    if (null == cfMapDef.getInheritanceType()) {
      return cfMapDef;
    }

    // if no columns we assume base class
    if (null == slice || null == slice.getColumns() || slice.getColumns().isEmpty()) {
      return cfMapDef;
    }

    // only support single table so use discriminator information

    String discColName = cfMapDef.getDiscColumn();
    DiscriminatorType discType = cfMapDef.getDiscType();
    Map<Object, CFMappingDef<? extends T, I>> derivedClasses = cfMapDef.getDerivedClassMap();

    // search for
    HColumn<String, byte[]> discCol = null;
    for (HColumn<String, byte[]> col : slice.getColumns()) {
      if (col.getName().equals(discColName)) {
        discCol = col;
        break;
      }
    }

    // if not found or empty value, then indicates use base class
    // TODO:BTB check for abstract base before allowing this
    if (null == discCol || 0 == discCol.getValue().length) {
      return cfMapDef;
    }

    // if discriminator column found, then lookup class type by
    // discriminator value
    Object discValue = convertColValueToDiscType(discType, discCol.getValue());
    CFMappingDef<? extends T, I> derivedCfMapDef = derivedClasses.get(discValue);
    if (null == derivedCfMapDef) {
      throw new RuntimeException("Cannot find derived class of " + cfMapDef.getClazz().getName()
          + " with discriminator value of " + discValue);
    }

    return derivedCfMapDef;
  }

  private Object convertColValueToDiscType(DiscriminatorType discType, byte[] value) {
    switch (discType) {
    case STRING:
      return new String(value);
    case CHAR:
      return ByteBuffer.wrap(value).asCharBuffer().get();
    case INTEGER:
      return IntegerSerializer.get().fromBytes(value);
    }

    throw new RuntimeException("must have added a new discriminator type, " + discType
        + ", because don't know how to convert db value - cannot continue");
  }

  private byte[] convertDiscTypeToColValue(DiscriminatorType discType, Object value) {
    switch (discType) {
    case STRING:
      return StringSerializer.get().toBytes((String) value);
    case CHAR:
      return String.valueOf((Character) value).getBytes();
    case INTEGER:
      return IntegerSerializer.get().toBytes((Integer) value);
    }

    throw new RuntimeException("must have added a new discriminator type, " + discType
        + ", because don't know how to convert db value - cannot continue");
  }

  private <T, I> void setIdIfCan(CFMappingDef<T, I> cfMapDef, T obj, I id)
      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
    PropertyMappingDefinition<I> md = cfMapDef.getIdPropertyDef();
    if (null == md) {
      throw new IllegalStateException(
          "Trying to build new object but haven't annotated a field with @Id");
    }

    Method meth = md.getPropDesc().getWriteMethod();
    if (null == meth) {
      logger.debug("@Id annotation found - but can't find setter for property, "
          + md.getPropDesc().getName());
    }

    meth.invoke(obj, id);
  }

  private <T, P> void setPropertyUsingColumn(T obj, HColumn<String, byte[]> col,
      PropertyMappingDefinition<P> md) throws IllegalArgumentException, IllegalAccessException,
      InvocationTargetException {
    PropertyDescriptor pd = md.getPropDesc();
    if (null == pd.getWriteMethod()) {
      throw new RuntimeException("property, " + pd.getName()
          + ", does not have a setter and therefore cannot be set");
    }

    @SuppressWarnings("unchecked")
    P value = md.getConverter().convertCassTypeToObjType((Class<P>) pd.getPropertyType(),
        col.getValue());
    pd.getWriteMethod().invoke(obj, value);
  }

  public static Serializer<?> determineSerializer(Class<?> theType) {
    Serializer<?> s = null;
    if (theType == Long.class || theType == long.class) {
      s = LongSerializer.get();
    } else if (theType == String.class) {
      s = StringSerializer.get();
    } else if (theType == Integer.class || theType == int.class) {
      s = IntegerSerializer.get();
    } else if (theType == UUID.class) {
      s = UUIDSerializer.get();
    } else if (theType == Boolean.class || theType == boolean.class) {
      s = BooleanSerializer.get();
    } else if (theType == Date.class) {
      s = DateSerializer.get();
    } else if (theType == byte[].class) {
      s = BytesArraySerializer.get();
    }
    // no float serializer at the moment
    // else if ( theType== Float.class) {
    // s = FloatSerializer.get();
    // }
    else if (theType == Double.class || theType == double.class) {
      s = DoubleSerializer.get();
    } else if (isSerializable(theType)) {
      s = ObjectSerializer.get();
    } else {
      throw new RuntimeException("unsupported property type, " + theType.getName());
    }
    return s;
  }

  public static boolean isSerializable(Class<?> clazz) {
    return isImplementedBy(clazz, Serializable.class);
  }

  public static boolean isImplementedBy(Class<?> clazz, Class<?> target) {
    if (null == clazz || null == target) {
      return false;
    }

    Class<?>[] interArr = clazz.getInterfaces();
    if (null == interArr) {
      return false;
    }

    for (Class<?> interfa : interArr) {
      if ( interfa.equals(target)) {
        return true;
      }
    }
    return false;
  }

  private void addToExtraIfCan(Object obj, HColumn<String, byte[]> col) throws SecurityException,
      NoSuchMethodException, IllegalArgumentException, IllegalAccessException,
      InvocationTargetException {
    Method meth = findAnnotatedMethod(obj.getClass(), AnonymousPropertyAddHandler.class);
    if (null == meth) {
      throw new IllegalArgumentException(
          "Object type, "
              + obj.getClass()
              + ", does not have a property named, "
              + col.getName()
              + ".  either add a setter for this property or use @AnonymousPropertyHandler to annotate a method for handling anonymous properties");
    }

    meth.invoke(obj, col.getName(), StringSerializer.get().fromBytes(col.getValue()));
  }

  private Method findAnnotatedMethod(Class<?> clazz, Class<? extends Annotation> anno) {
    for (Method meth : clazz.getMethods()) {
      if (meth.isAnnotationPresent(anno)) {
        return meth;
      }
    }
    return null;
  }

}
TOP

Related Classes of me.prettyprint.hom.HectorObjectMapper

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.