Java tutorial
/* * 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; } }