
me.prettyprint.hom.HectorObjectMapper Maven / Gradle / Ivy
package me.prettyprint.hom;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
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.AnonymousPropertyCollectionGetter;
import me.prettyprint.hom.cache.HectorObjectMapperException;
import me.prettyprint.hom.converters.Converter;
import me.prettyprint.hom.converters.DefaultConverter;
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;
private KeyConcatenationStrategy keyConcatStrategy = new KeyConcatenationDelimiterStrategyImpl();
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
*
* @param keyspace
* @param colFamName
* @param pkObj
* @return
*/
public T getObject(Keyspace keyspace, String colFamName, Object pkObj) {
if (null == pkObj) {
throw new IllegalArgumentException("object ID cannot be null or empty");
}
CFMappingDef cfMapDef = cacheMgr.getCfMapDef(colFamName, true);
byte[] colFamKey = generateColumnFamilyKeyFromPkObj(cfMapDef, pkObj);
SliceQuery q = HFactory.createSliceQuery(keyspace,
BytesArraySerializer.get(), StringSerializer.get(), BytesArraySerializer.get());
q.setColumnFamily(colFamName);
q.setKey(colFamKey);
// if no anonymous handler then use specific columns
if (cfMapDef.isSliceColumnArrayRequired()) {
q.setColumnNames(cfMapDef.getSliceColumnNameArr());
} else {
q.setRange("", "", false, maxNumColumns);
}
QueryResult> result = q.execute();
if (null == result || null == result.get()) {
return null;
}
T obj = createObject(cfMapDef, pkObj, 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);
byte[] colFamKey = generateColumnFamilyKeyFromPojo(obj, cfMapDef);
Collection> colColl = createColumnSet(obj);
String colFamName = cfMapDef.getEffectiveColFamName();
Mutator m = HFactory.createMutator(keyspace, BytesArraySerializer.get());
for (HColumn col : colColl) {
if ( null == col.getName() || col.getName().isEmpty() ) {
throw new HectorObjectMapperException("Column name cannot be null or empty - trying to persist to ColumnFamily, " + colFamName);
}
m.addInsertion(colFamKey, colFamName, col);
}
m.execute();
return obj;
}
private byte[] generateColumnFamilyKeyFromPkObj(CFMappingDef> cfMapDef, Object pkObj) {
List segmentList = new ArrayList(cfMapDef.getKeyDef().getIdPropertyMap().size());
if (cfMapDef.getKeyDef().isComplexKey()) {
for (PropertyDescriptor pd : cfMapDef.getKeyDef().getPropertyDescriptorMap().values()) {
segmentList.add(callMethodAndConvertToCassandraType(pkObj, pd.getReadMethod(),
new DefaultConverter()));
}
}
else {
PropertyMappingDefinition md = cfMapDef.getKeyDef().getIdPropertyMap().values().iterator().next();
segmentList.add(md.getConverter().convertObjTypeToCassType(pkObj));
}
return keyConcatStrategy.concat(segmentList);
}
private byte[] generateColumnFamilyKeyFromPojo(Object obj, CFMappingDef> cfMapDef) {
List segmentList = new ArrayList(cfMapDef.getKeyDef().getIdPropertyMap().size());
for (PropertyMappingDefinition md : cfMapDef.getKeyDef().getIdPropertyMap().values()) {
Method meth = md.getPropDesc().getReadMethod();
segmentList.add(callMethodAndConvertToCassandraType(obj, meth, md.getConverter()));
}
return keyConcatStrategy.concat(segmentList);
}
private byte[] callMethodAndConvertToCassandraType(Object obj, Method meth, Converter converter) {
try {
Object retVal = meth.invoke(obj, (Object[]) null);
return converter.convertObjTypeToCassType(retVal);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
/**
* 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, Object pkObj, ColumnSlice slice) {
if (slice.getColumns().isEmpty()) {
return null;
}
CFMappingDef extends T> cfMapDefInstance = determineClassType(cfMapDef, slice);
try {
T obj = cfMapDefInstance.getEffectiveClass().newInstance();
setIdIfCan(cfMapDef, obj, pkObj);
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, cfMapDef, 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);
}
}
/**
* 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> 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 = cacheMgr.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> 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