
me.prettyprint.hom.HectorObjectMapper Maven / Gradle / Ivy
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 javax.persistence.Id;
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 me.prettyprint.hom.cache.HectorObjectMapperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Maps a slice of HColumn
s to an object's
* properties. See {@link #createObject(Object, ColumnSlice)} for more details.
*
* As mentioned above all column names must be String
s - doesn't
* really make sense to have other types when mapping to object properties.
*
* @param
* 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 getObject(Keyspace keyspace, String colFamName, Class clazz, I id) {
if (null == id) {
throw new IllegalArgumentException("object ID cannot be null or empty");
}
CFMappingDef cfMapDef = cacheMgr.getCfMapDef(colFamName, true);
PropertyMappingDefinition md = cfMapDef.getIdPropertySet().iterator().next();
if (null == md) {
throw new HectorObjectMapperException(
"Trying to build new object but haven't annotated a field with @" + Id.class.getSimpleName());
}
byte[] idAsBytes = md.getConverter().convertObjTypeToCassType(id);
SliceQuery q = HFactory.createSliceQuery(keyspace,
BytesArraySerializer.get(), StringSerializer.get(), BytesArraySerializer.get());
q.setColumnFamily(colFamName);
q.setKey(idAsBytes);
q.setRange("", "", false, maxNumColumns);
QueryResult> result = q.execute();
if (null == result || null == result.get()) {
return null;
}
T obj = createObject(cfMapDef, id, result.get());
return obj;
}
public T saveObj(Keyspace keyspace, T obj) {
if (null == obj) {
throw new IllegalArgumentException("object cannot be null");
}
@SuppressWarnings("unchecked")
CFMappingDef cfMapDef = (CFMappingDef) cacheMgr.getCfMapDef(obj.getClass(), true);
PropertyMappingDefinition md = cfMapDef.getIdPropertySet().iterator().next();
if (null == md) {
throw new HectorObjectMapperException(
"Trying to save object but haven't annotated a field with @" + Id.class.getSimpleName());
}
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> colColl = createColumnSet(obj);
String colFamName = cfMapDef.getEffectiveColFamName();
Mutator m = HFactory.createMutator(keyspace, BytesArraySerializer.get());
for (HColumn 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
* ColumnSlice
*
* @return instantiated object if success, null if slice is empty,
* RuntimeException otherwise
*/
T createObject(CFMappingDef cfMapDef, I id, ColumnSlice slice) {
if (slice.getColumns().isEmpty()) {
return null;
}
CFMappingDef extends T, I> cfMapDefInstance = determineClassType(cfMapDef, slice);
try {
T obj = cfMapDefInstance.getEffectiveClass().newInstance();
setIdIfCan(cfMapDef, obj, id);
for (HColumn 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> createColumnSet(Object obj) {
Map> 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
*/
Map> createColumnMap(T obj) {
if (null == obj) {
throw new IllegalArgumentException("Class type cannot be null");
}
@SuppressWarnings("unchecked")
CFMappingDef cfMapDef = (CFMappingDef) cacheMgr.getCfMapDef(
(Class) obj.getClass(), true);
try {
Map> colSet = new HashMap>();
Collection> coll = cfMapDef.getAllProperties();
for (PropertyMappingDefinition> md : coll) {
HColumn 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> colSet)
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Method meth = findAnnotatedMethod(obj.getClass(), AnonymousPropertyCollectionGetter.class);
if (null == meth) {
return;
}
@SuppressWarnings("unchecked")
Collection> propColl = (Collection>) meth.invoke(
obj, (Object[]) null);
if (null == propColl || propColl.isEmpty()) {
return;
}
for (Entry entry : propColl) {
colSet.put(entry.getKey(), HFactory.createColumn(entry.getKey(), entry.getValue().getBytes(),
StringSerializer.get(), BytesArraySerializer.get()));
}
}
private HColumn createColumnFromProperty(T obj,
PropertyMappingDefinition md) throws SecurityException, NoSuchFieldException,
IllegalArgumentException, IllegalAccessException, InvocationTargetException {
byte[] colValue = createBytesFromPropertyValue(obj, md);
if (null == colValue) {
return null;
}
HColumn col = createHColumn(md.getColName(), colValue);
return col;
}
private byte[] createBytesFromPropertyValue(Object obj, PropertyMappingDefinition
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 createHColumn(String name, byte[] value) {
return HFactory.createColumn(name, value, StringSerializer.get(), BytesArraySerializer.get());
}
private CFMappingDef extends T, I> determineClassType(CFMappingDef cfMapDef,
ColumnSlice 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