ei.ne.ke.cassandra.cql3.EntitySpecificationUtils.java Source code

Java tutorial

Introduction

Here is the source code for ei.ne.ke.cassandra.cql3.EntitySpecificationUtils.java

Source

/*
 * Copyright 2013 EK3 Technologies, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ei.ne.ke.cassandra.cql3;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import javax.persistence.IdClass;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.ReflectionUtils.AnnotationFieldFilter;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.netflix.astyanax.Serializer;
import com.netflix.astyanax.model.Column;
import com.netflix.astyanax.model.ColumnFamily;
import com.netflix.astyanax.serializers.StringSerializer;

/**
 * Utilities used by implementations of {@link EntitySpecification}.
 */
public final class EntitySpecificationUtils {

    public static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.wrap(new byte[0]);

    private static final Logger LOGGER = LoggerFactory.getLogger(EntitySpecificationUtils.class);
    private static final int BOOLEAN_BEAN_PROPERTY_ACCESSOR_PREFIX_LENGTH = 2;
    private static final int NON_BOOLEAN_BEAN_PROPERTY_ACCESSOR_PREFIX_LENGTH = 3;

    /**
     * Constructor.
     */
    private EntitySpecificationUtils() {
    }

    /**
     * @param name the name of the column to normalize
     * @return the normalized name of the column
     */
    public static String normalizeCqlElementName(String name) {
        Preconditions.checkNotNull(name);
        return name.toLowerCase();
    }

    /**
     * @param entityClazz
     * @return the value of the {@link java.reflect.Field} annotated with
     * {@link javax.persistence.Id}.
     */
    public static <T, ID extends Serializable> Field findPrimaryKeyField(Class<T> entityClazz) {
        Preconditions.checkNotNull(entityClazz);
        Field f = ReflectionUtils.findField(entityClazz, new AnnotationFieldFilter(Id.class));
        if (f != null && !f.isAccessible()) {
            f.setAccessible(true);
        }
        return f;
    }

    /**
     * @param entityClazz
     * @return the value of the {@link java.reflect.Field} annotated with
     * {@link javax.persistence.EmbeddedId}.
     */
    public static <T, ID extends Serializable> Field findCompoundKeyField(Class<T> entityClazz) {
        Preconditions.checkNotNull(entityClazz);
        Field f = ReflectionUtils.findField(entityClazz, new AnnotationFieldFilter(EmbeddedId.class));
        if (f != null && !f.isAccessible()) {
            f.setAccessible(true);
        }
        return f;
    }

    /**
     * If exactly one field is annotated with @EmbeddedId or the class is
     * annotated with @IdClass, we consider the entity a compound entity (= the
     * table has a compound key).  Otherwise if exactly one field is annotated
     * with @Id, we consider the entity a simple entity (= the table has a
     * primary key).
     *
     * @param entityClazz
     * @return what type of entity the annotated class is (simple or compound).
     */
    public static <T> SupportedEntityType inspectEntityClass(Class<T> entityClazz) {
        Preconditions.checkNotNull(entityClazz);
        boolean idClass = AnnotationUtils.isAnnotationDeclaredLocally(IdClass.class, entityClazz);
        Field id = findPrimaryKeyField(entityClazz);
        Field embeddedId = findCompoundKeyField(entityClazz);
        if (idClass) {
            if (embeddedId != null) {
                throw new IllegalStateException("@IdClass and @EmbeddedId are mutually exclusive.");
            }
            return SupportedEntityType.COMPOUND_IDCLASS;
        }
        if (id != null && embeddedId != null) {
            throw new IllegalStateException("@Id and @EmbeddedId are mutually exclusive");
        } else if (id == null && embeddedId == null) {
            throw new IllegalStateException("Entity needs at least one of @Id, @EmbeddedId, or @IdClass");
        } else if (id != null) {
            return SupportedEntityType.SIMPLE;
        } else if (embeddedId != null) {
            /*
             * If the field is annotated with @EmbeddedId, then the type of the
             * field must have been annotated with @Embeddable.
             */
            Class<?> embeddedType = embeddedId.getType();
            boolean annotatedWithEmbeddable = false;
            for (Annotation annotation : embeddedType.getDeclaredAnnotations()) {
                if (annotation.annotationType().equals(javax.persistence.Embeddable.class)) {
                    annotatedWithEmbeddable = true;
                    break;
                }
            }
            if (!annotatedWithEmbeddable) {
                throw new IllegalStateException("Embedded entity isn't annotated with @Embeddable.");
            }
            return SupportedEntityType.COMPOUND;
        } else {
            return SupportedEntityType.NONE;
        }
    }

    /**
     * Creates a column family suitable for use with the entity. We try to
     * extract the name of the CQL3 table from a {@link javax.persistence.Table}
     * annotation. If the annotation is absent or the "name" parameter is null,
     * we will use the "name" parameter of the {@link javax.persistence.Entity}
     * annotation. If that name is null, we will use the name of the class.
     *
     * @param entityClazz
     * @return a column family object suitable for use with the given entity.
     */
    public static <T> ColumnFamily<String, String> inferColumnFamily(Class<T> entityClazz) {
        Preconditions.checkNotNull(entityClazz);
        String tableName = null;
        for (Annotation annotation : entityClazz.getDeclaredAnnotations()) {
            Class<? extends Annotation> annotationType = annotation.annotationType();
            /*
             * @Table has the highest priority. Then follows @Entity. Lastly,
             * the name of the entity's class.
             */
            if (annotationType.equals(javax.persistence.Table.class)) {
                javax.persistence.Table table = ((javax.persistence.Table) annotation);
                if (table.name() != null) {
                    tableName = table.name();
                }
            } else if (annotationType.equals(javax.persistence.Entity.class)) {
                javax.persistence.Entity entity = ((javax.persistence.Entity) annotation);
                if (tableName == null && entity.name() != null) {
                    tableName = entity.name();
                }
            }
        }
        if (tableName == null) {
            tableName = entityClazz.getSimpleName();
        }
        tableName = normalizeCqlElementName(tableName);
        ColumnFamily<String, String> columnFamily = ColumnFamily.newColumnFamily(tableName, StringSerializer.get(),
                StringSerializer.get());
        return columnFamily;
    }

    /**
     * @param primaryKeyField
     * @return the column mapping to the {@link java.reflect.Field} annotated
     * with {@link javax.persistence.Id} and {@link javax.persistence.Column}
     *
     * @see http://docs.oracle.com/javaee/6/api/index.html?javax/persistence/Id.html
     */
    public static String getPrimaryKeyColumnName(Field primaryKeyField) {
        Preconditions.checkNotNull(primaryKeyField);
        javax.persistence.Column column = primaryKeyField.getAnnotation(javax.persistence.Column.class);
        if (column == null) {
            /*
             * The documentation for @Id states that <quote>If no Column
             * annotation is specified, the primary key column name is assumed
             * to be the name of the primary key property or field.</quote>
             */
            return primaryKeyField.getName();
        } else {
            return normalizeCqlElementName(column.name());
        }
    }

    /**
     * @param embeddedType
     * @return the column mapping of the {@link java.reflect.Field} annotated with
     * {@link javax.persistence.EmbeddedId}.
     *
     * @see http://docs.oracle.com/javaee/6/api/index.html?javax/persistence/EmbeddedId.html
     */
    public static <T> List<String> getCompoundKeyColumnNames(Class<T> embeddedType) {
        Preconditions.checkNotNull(embeddedType);
        List<String> columns = Lists.newArrayList();
        for (Field embeddedField : embeddedType.getDeclaredFields()) {
            javax.persistence.Column c = embeddedField.getAnnotation(javax.persistence.Column.class);
            if (c == null) {
                continue;
            }
            String normalizedName = normalizeCqlElementName(c.name());
            if (columns.contains(normalizedName)) {
                throw new IllegalStateException(String.format("Duplicate column name '%s'", normalizedName));
            }
            columns.add(normalizedName);
        }
        for (Method embeddedMethod : embeddedType.getDeclaredMethods()) {
            javax.persistence.Column c = embeddedMethod.getAnnotation(javax.persistence.Column.class);
            if (c == null) {
                continue;
            }
            String normalizedName = normalizeCqlElementName(c.name());
            if (columns.contains(normalizedName)) {
                throw new IllegalStateException(String.format("Duplicate column name '%s'", normalizedName));
            }
            columns.add(normalizedName);
        }
        return columns;
    }

    /**
     *
     *
     * @param entityClazz
     * @param propertyName
     * @return
     */
    public static <T> Method findGetterMethod(Class<T> entityClazz, String propertyName) {
        for (Method methodCandidate : entityClazz.getDeclaredMethods()) {
            String methodCandidateName = methodCandidate.getName();
            if ((methodCandidateName.startsWith("get") || methodCandidateName.startsWith("is"))
                    && methodCandidateName.endsWith(propertyName)) {
                return methodCandidate;
            }
        }
        return null;
    }

    /**
     *
     *
     * @param entityClazz
     * @param propertyName
     * @return
     */
    public static <T> Method findSetterMethod(Class<T> entityClazz, String propertyName) {
        for (Method methodCandidate : entityClazz.getDeclaredMethods()) {
            String methodCandidateName = methodCandidate.getName();
            if (methodCandidateName.startsWith("set") && methodCandidateName.endsWith(propertyName)) {
                return methodCandidate;
            }
        }
        return null;
    }

    /**
     * Constructs a map of {@link String}S representing column names to {@link
     * Field}S.
     *
     * @param entityClazz
     * @return
     */
    public static <T> Map<String, AttributeAccessor> createAccessors(Class<T> entityClazz) {
        Preconditions.checkNotNull(entityClazz);
        Map<String, AttributeAccessor> map = Maps.newLinkedHashMap();
        for (Field f : entityClazz.getDeclaredFields()) {
            javax.persistence.Column c = f.getAnnotation(javax.persistence.Column.class);
            if (c == null) {
                continue;
            }
            String normalizedName = normalizeCqlElementName(c.name());
            if (map.containsKey(normalizedName)) {
                throw new IllegalStateException(String.format("Duplicate column name '%s'", normalizedName));
            }
            AttributeAccessor accessor = new AttributeAccessorFieldIntrospection(f);
            map.put(normalizedName, accessor);
        }
        for (Method firstMethod : entityClazz.getDeclaredMethods()) {
            javax.persistence.Column c = firstMethod.getAnnotation(javax.persistence.Column.class);
            if (c == null) {
                continue;
            }
            String normalizedName = normalizeCqlElementName(c.name());
            /*
             * For bean getters and setters, we allow to annotate either the
             * getter or the setter and will find the corresponding counterpart.
             * If the column name is already mapped, then there's either a
             * @Column-annotated field, or the previous reversed pair of getters
             * and setters was found. Either way, ignore this annotated method.
             */
            if (map.containsKey(normalizedName)) {
                continue;
            }
            String firstMethodName = firstMethod.getName();
            AttributeAccessor accessor = null;

            if (firstMethodName.startsWith("set")) {
                String attributeName = firstMethodName.substring(NON_BOOLEAN_BEAN_PROPERTY_ACCESSOR_PREFIX_LENGTH);
                Method secondMethod = findGetterMethod(entityClazz, attributeName);
                if (secondMethod == null) {
                    throw new IllegalArgumentException(String.format("No counterpart to %s", firstMethodName));
                }
                accessor = new AttributeAccessorBeanMethods(secondMethod, firstMethod);
            } else if (firstMethodName.startsWith("is")) {
                String attributeName = firstMethodName.substring(BOOLEAN_BEAN_PROPERTY_ACCESSOR_PREFIX_LENGTH);
                Method secondMethod = findSetterMethod(entityClazz, attributeName);
                if (secondMethod == null) {
                    throw new IllegalArgumentException(String.format("No counterpart to %s", firstMethodName));
                }
                accessor = new AttributeAccessorBeanMethods(firstMethod, secondMethod);
            } else if (firstMethodName.startsWith("get")) {
                String attributeName = firstMethodName.substring(NON_BOOLEAN_BEAN_PROPERTY_ACCESSOR_PREFIX_LENGTH);
                /*
                 * Find the corresponding setter.
                 */
                Method secondMethod = findSetterMethod(entityClazz, attributeName);
                if (secondMethod == null) {
                    throw new IllegalArgumentException(String.format("No counterpart to %s", firstMethodName));
                }
                accessor = new AttributeAccessorBeanMethods(firstMethod, secondMethod);
            }
            map.put(normalizedName, accessor);
        }

        return map;
    }

    /**
     *
     *
     * @param field
     * @param entity
     * @return
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static <T> ByteBuffer serializeAttribute(AttributeAccessor accessor, T entity) {
        Preconditions.checkNotNull(accessor);
        Preconditions.checkNotNull(entity);
        Serializer serializer = SerializerFactory.inferSerializer(accessor);
        Object value = accessor.get(entity);
        if (value == null) {
            /*
             * While the column value of a collection type column is null, the
             * column representation isn't necessarily the empty byte buffer. If
             * we have a null value for collection type on the Java side, we
             * must marshal the corresponding empty collection type to a
             * ByteBuffer representation to set the column value.
             */
            if (List.class.isAssignableFrom(accessor.getClazz())) {
                return serializer.toByteBuffer(Collections.EMPTY_LIST);
            } else if (Map.class.isAssignableFrom(accessor.getClazz())) {
                return serializer.toByteBuffer(Collections.EMPTY_MAP);
            } else if (Set.class.isAssignableFrom(accessor.getClazz())) {
                return serializer.toByteBuffer(Collections.EMPTY_SET);
            } else {
                return EMPTY_BYTE_BUFFER;
            }
        }
        ByteBuffer buf = serializer.toByteBuffer(value);
        return buf;
    }

    /**
     *
     *
     * @param column
     * @param field
     * @param entity
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static <T> void deserializeAttribute(Column<String> column, AttributeAccessor accessor, T entity) {
        Preconditions.checkNotNull(column);
        Preconditions.checkNotNull(accessor);
        Preconditions.checkNotNull(entity);
        Object value = null;
        if (column.hasValue()) {
            Serializer serializer = SerializerFactory.inferSerializer(accessor);
            value = column.getValue(serializer);
        } else {
            /*
             * We can't distinguish between an empty collection type column and
             * a collection type column set to null. We up-convert those to
             * their corresponding empty collection type.
             */
            if (List.class.isAssignableFrom(accessor.getClazz())) {
                value = Lists.newArrayList();
            } else if (Map.class.isAssignableFrom(accessor.getClazz())) {
                value = Maps.newHashMap();
            } else if (Set.class.isAssignableFrom(accessor.getClazz())) {
                value = Sets.newHashSet();
            }
        }
        accessor.set(entity, value);
    }

    /**
     * @param key the key whose key values set will be determined
     * @return a list of key columns that are set.
     */
    public static List<String> getKeysSet(Map<String, ByteBuffer> values) {
        Preconditions.checkNotNull(values);
        List<String> result = new ArrayList<String>();
        for (Map.Entry<String, ByteBuffer> entry : values.entrySet()) {
            String column = entry.getKey();
            ByteBuffer value = entry.getValue();
            if (value == EMPTY_BYTE_BUFFER) {
                continue;
            }
            result.add(column);
        }
        return result;
    }

}