package com.netflix.astyanax.entitystore;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.PersistenceException;
import org.apache.commons.lang.StringUtils;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.netflix.astyanax.ColumnListMutation;
import com.netflix.astyanax.model.ColumnList;
import com.netflix.astyanax.model.Equality;
import com.netflix.astyanax.query.ColumnPredicate;
/**
* Mapper from a CompositeType to an embedded entity. The composite entity is expected
* to have an @Id annotation for each composite component and a @Column annotation for
* the value.
*
* @author elandau
*
*/
public class CompositeColumnEntityMapper {
/**
* Class of embedded entity
*/
private final Class<?> clazz;
/**
* List of serializers for the composite parts
*/
private List<FieldMapper<?>> components = Lists.newArrayList();
/**
* List of valid (i.e. existing) column names
*/
private Set<String> validNames = Sets.newHashSet();
/**
* Mapper for the value part of the entity
*/
private FieldMapper<?> valueMapper;
/**
* Largest buffer size
*/
private int bufferSize = 64;
/**
* Parent field
*/
private final Field containerField;
public CompositeColumnEntityMapper(Field field) {
ParameterizedType containerEntityType = (ParameterizedType) field.getGenericType();
this.clazz = (Class<?>) containerEntityType.getActualTypeArguments()[0];
this.containerField = field;
this.containerField.setAccessible(true);
Field[] declaredFields = clazz.getDeclaredFields();
for (Field f : declaredFields) {
// The value
Column columnAnnotation = f.getAnnotation(Column.class);
if ((columnAnnotation != null)) {
f.setAccessible(true);
FieldMapper fieldMapper = new FieldMapper(f);
components.add(fieldMapper);
validNames.add(fieldMapper.getName());
}
}
// Last one is always treated as the 'value'
valueMapper = components.remove(components.size() - 1);
}
/**
* Iterate through the list and create a column for each element
* @param clm
* @param entity
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public void fillMutationBatch(ColumnListMutation<ByteBuffer> clm, Object entity) throws IllegalArgumentException, IllegalAccessException {
List<?> list = (List<?>) containerField.get(entity);
if (list != null) {
for (Object element : list) {
fillColumnMutation(clm, element);
}
}
}
public void fillMutationBatchForDelete(ColumnListMutation<ByteBuffer> clm, Object entity) throws IllegalArgumentException, IllegalAccessException {
List<?> list = (List<?>) containerField.get(entity);
if (list == null) {
clm.delete();
}
else {
for (Object element : list) {
clm.deleteColumn(toColumnName(element));
}
}
}
/**
* Add a column based on the provided entity
*
* @param clm
* @param entity
*/
public void fillColumnMutation(ColumnListMutation<ByteBuffer> clm, Object entity) {
try {
ByteBuffer columnName = toColumnName(entity);
ByteBuffer value = valueMapper.toByteBuffer(entity);
clm.putColumn(columnName, value);
} catch(Exception e) {
throw new PersistenceException("failed to fill mutation batch", e);
}
}
/**
* Return the column name byte buffer for this entity
*
* @param obj
* @return
*/
public ByteBuffer toColumnName(Object obj) {
SimpleCompositeBuilder composite = new SimpleCompositeBuilder(bufferSize, Equality.EQUAL);
// Iterate through each component and add to a CompositeType structure
for (FieldMapper<?> mapper : components) {
try {
composite.addWithoutControl(mapper.toByteBuffer(obj));
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
return composite.get();
}
/**
* Set the collection field using the provided column list of embedded entities
* @param entity
* @param name
* @param column
* @return
* @throws Exception
*/
public boolean setField(Object entity, ColumnList<ByteBuffer> columns) throws Exception {
List<Object> list = getOrCreateField(entity);
// Iterate through columns and add embedded entities to the list
for (com.netflix.astyanax.model.Column<ByteBuffer> c : columns) {
list.add(fromColumn(c));
}
return true;
}
public boolean setFieldFromCql(Object entity, ColumnList<ByteBuffer> columns) throws Exception {
List<Object> list = getOrCreateField(entity);
// Iterate through columns and add embedded entities to the list
// for (com.netflix.astyanax.model.Column<ByteBuffer> c : columns) {
list.add(fromCqlColumns(columns));
// }
return true;
}
private List<Object> getOrCreateField(Object entity) throws IllegalArgumentException, IllegalAccessException {
// Get or create the list field
List<Object> list = (List<Object>) containerField.get(entity);
if (list == null) {
list = Lists.newArrayList();
containerField.set(entity, list);
}
return list;
}
/**
* Return an object from the column
*
* @param cl
* @return
*/
public Object fromColumn(com.netflix.astyanax.model.Column<ByteBuffer> c) {
try {
// Allocate a new entity
Object entity = clazz.newInstance();
setEntityFieldsFromColumnName(entity, c.getRawName().duplicate());
valueMapper.setField(entity, c.getByteBufferValue().duplicate());
return entity;
} catch(Exception e) {
throw new PersistenceException("failed to construct entity", e);
}
}
public Object fromCqlColumns(com.netflix.astyanax.model.ColumnList<ByteBuffer> c) {
try {
// Allocate a new entity
Object entity = clazz.newInstance();
Iterator<com.netflix.astyanax.model.Column<ByteBuffer>> columnIter = c.iterator();
columnIter.next();
for (FieldMapper<?> component : components) {
component.setField(entity, columnIter.next().getByteBufferValue());
}
valueMapper.setField(entity, columnIter.next().getByteBufferValue());
return entity;
} catch(Exception e) {
throw new PersistenceException("failed to construct entity", e);
}
}
/**
*
* @param entity
* @param columnName
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public void setEntityFieldsFromColumnName(Object entity, ByteBuffer columnName) throws IllegalArgumentException, IllegalAccessException {
// Iterate through components in order and set fields
for (FieldMapper<?> component : components) {
ByteBuffer data = getWithShortLength(columnName);
if (data != null) {
if (data.remaining() > 0) {
component.setField(entity, data);
}
byte end_of_component = columnName.get();
if (end_of_component != Equality.EQUAL.toByte()) {
throw new RuntimeException("Invalid composite column. Expected END_OF_COMPONENT.");
}
}
else {
throw new RuntimeException("Missing component data in composite type");
}
}
}
/**
* Return the cassandra comparator type for this composite structure
* @return
*/
public String getComparatorType() {
StringBuilder sb = new StringBuilder();
sb.append("CompositeType(");
sb.append(StringUtils.join(
Collections2.transform(components, new Function<FieldMapper<?>, String>() {
public String apply(FieldMapper<?> input) {
return input.serializer.getComparatorType().getClassName();
}
}),
","));
sb.append(")");
return sb.toString();
}
public static int getShortLength(ByteBuffer bb) {
int length = (bb.get() & 0xFF) << 8;
return length | (bb.get() & 0xFF);
}
public static ByteBuffer getWithShortLength(ByteBuffer bb) {
int length = getShortLength(bb);
return getBytes(bb, length);
}
public static ByteBuffer getBytes(ByteBuffer bb, int length) {
ByteBuffer copy = bb.duplicate();
copy.limit(copy.position() + length);
bb.position(bb.position() + length);
return copy;
}
public String getValueType() {
return valueMapper.getSerializer().getComparatorType().getClassName();
}
public ByteBuffer[] getQueryEndpoints(Collection<ColumnPredicate> predicates) {
// Convert to multimap for easy lookup
ArrayListMultimap<Object, ColumnPredicate> lookup = ArrayListMultimap.create();
for (ColumnPredicate predicate : predicates) {
Preconditions.checkArgument(validNames.contains(predicate.getName()), "Field '" + predicate.getName() + "' does not exist in the entity " + clazz.getCanonicalName());
lookup.put(predicate.getName(), predicate);
}
SimpleCompositeBuilder start = new SimpleCompositeBuilder(bufferSize, Equality.GREATER_THAN_EQUALS);
SimpleCompositeBuilder end = new SimpleCompositeBuilder(bufferSize, Equality.LESS_THAN_EQUALS);
// Iterate through components in order while applying predicate to 'start' and 'end'
for (FieldMapper<?> mapper : components) {
for (ColumnPredicate p : lookup.get(mapper.getName())) {
applyPredicate(mapper, start, end, p);
}
}
return new ByteBuffer[]{start.get(), end.get()};
}
private void applyPredicate(FieldMapper<?> mapper, SimpleCompositeBuilder start, SimpleCompositeBuilder end, ColumnPredicate predicate) {
ByteBuffer bb = mapper.valueToByteBuffer(predicate.getValue());
switch (predicate.getOp()) {
case EQUAL:
start.addWithoutControl(bb);
end.addWithoutControl(bb);
break;
case GREATER_THAN:
case GREATER_THAN_EQUALS:
if (mapper.isAscending())
start.add(bb, predicate.getOp());
else
end.add(bb, predicate.getOp());
break;
case LESS_THAN:
case LESS_THAN_EQUALS:
if (mapper.isAscending())
end.add(bb, predicate.getOp());
else
start.add(bb, predicate.getOp());
break;
}
}
}