All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.astyanax.entitystore.CompositeColumnEntityMapper Maven / Gradle / Ivy

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>    components = Lists.newArrayList();
    
    /**
     * List of valid (i.e. existing) column names
     */
    private Set             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 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 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 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 columns) throws Exception {
        List list = getOrCreateField(entity);
            
        // Iterate through columns and add embedded entities to the list
        for (com.netflix.astyanax.model.Column c : columns) {
            list.add(fromColumn(c));
        }
        
        return true;
    }
    
    public boolean setFieldFromCql(Object entity, ColumnList columns) throws Exception {
        List list = getOrCreateField(entity);
    
        // Iterate through columns and add embedded entities to the list
//        for (com.netflix.astyanax.model.Column c : columns) {
            list.add(fromCqlColumns(columns));
//       }
        
        return true;
    }
    
    private List getOrCreateField(Object entity) throws IllegalArgumentException, IllegalAccessException {
        // Get or create the list field
        List list = (List) 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 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 c) {
        try {
            // Allocate a new entity
            Object entity         = clazz.newInstance();
            
            Iterator> 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, 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 predicates) {
        // Convert to multimap for easy lookup
        ArrayListMultimap 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;
        }
    }
}