package com.netflix.astyanax.mapping; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; 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.Row; import com.netflix.astyanax.model.Rows; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * <p> * Utility for doing object/relational mapping between bean-like instances and * Cassandra * </p> * <p/> * <p> * The mapper stores values in Cassandra and maps in/out to native types. Column * names must be strings. Annotate your bean with {@link Id} and {@link Column}. * Or, provide an {@link AnnotationSet} that defines IDs and Columns in your * bean. */ @SuppressWarnings({ "SuspiciousMethodCalls" }) public class Mapping<T> { private final ImmutableMap<String, Field> fields; private final String idFieldName; private final Class<T> clazz; /** * If the ID column does not have a Column annotation, this column name is * used */ public static final String DEFAULT_ID_COLUMN_NAME = "ID"; /** * Convenience for allocation a mapping object * * @param clazz * clazz type to map * @return mapper */ public static <T> Mapping<T> make(Class<T> clazz) { return new Mapping<T>(clazz, new DefaultAnnotationSet()); } /** * Convenience for allocation a mapping object * * @param clazz * clazz type to map * @param annotationSet * annotations to use when analyzing a bean * @return mapper */ public static <T> Mapping<T> make(Class<T> clazz, AnnotationSet<?, ?> annotationSet) { return new Mapping<T>(clazz, annotationSet); } /** * @param clazz * clazz type to map */ public Mapping(Class<T> clazz) { this(clazz, new DefaultAnnotationSet()); } /** * @param clazz * clazz type to map * @param annotationSet * annotations to use when analyzing a bean */ public Mapping(Class<T> clazz, AnnotationSet<?, ?> annotationSet) { this.clazz = clazz; String localKeyFieldName = null; ImmutableMap.Builder<String, Field> builder = ImmutableMap.builder(); AtomicBoolean isKey = new AtomicBoolean(); Set<String> usedNames = Sets.newHashSet(); for (Field field : clazz.getDeclaredFields()) { String name = mapField(field, annotationSet, builder, usedNames, isKey); if (isKey.get()) { Preconditions.checkArgument(localKeyFieldName == null); localKeyFieldName = name; } } Preconditions.checkNotNull(localKeyFieldName); fields = builder.build(); idFieldName = localKeyFieldName; } /** * Return the value for the ID/Key column from the given instance * * @param instance * the instance * @param valueClass * type of the value (must match the actual native type in the * instance's class) * @return value */ public <V> V getIdValue(T instance, Class<V> valueClass) { return getColumnValue(instance, idFieldName, valueClass); } /** * Return the value for the given column from the given instance * * @param instance * the instance * @param columnName * name of the column (must match a corresponding annotated field * in the instance's class) * @param valueClass * type of the value (must match the actual native type in the * instance's class) * @return value */ public <V> V getColumnValue(T instance, String columnName, Class<V> valueClass) { Field field = fields.get(columnName); if (field == null) { throw new IllegalArgumentException("Column not found: " + columnName); } try { return valueClass.cast(field.get(instance)); } catch (IllegalAccessException e) { throw new RuntimeException(e); // should never get here } } /** * Set the value for the ID/Key column for the given instance * * @param instance * the instance * @param value * The value (must match the actual native type in the instance's * class) */ public <V> void setIdValue(T instance, V value) { setColumnValue(instance, idFieldName, value); } /** * Set the value for the given column for the given instance * * @param instance * the instance * @param columnName * name of the column (must match a corresponding annotated field * in the instance's class) * @param value * The value (must match the actual native type in the instance's * class) */ public <V> void setColumnValue(T instance, String columnName, V value) { Field field = fields.get(columnName); if (field == null) { throw new IllegalArgumentException("Column not found: " + columnName); } try { field.set(instance, value); } catch (IllegalAccessException e) { throw new RuntimeException(e); // should never get here } } /** * Map a bean to a column mutation. i.e. set the columns in the mutation to * the corresponding values from the instance * * @param instance * instance * @param mutation * mutation */ public void fillMutation(T instance, ColumnListMutation<String> mutation) { for (String fieldName : getNames()) { Coercions.setColumnMutationFromField(instance, fields.get(fieldName), fieldName, mutation); } } /** * Allocate a new instance and populate it with the values from the given * column list * * @param columns * column list * @return the allocated instance * @throws IllegalAccessException * if a new instance could not be instantiated * @throws InstantiationException * if a new instance could not be instantiated */ public T newInstance(ColumnList<String> columns) throws IllegalAccessException, InstantiationException { return initInstance(clazz.newInstance(), columns); } /** * Populate the given instance with the values from the given column list * * @param instance * instance * @param columns * column this * @return instance (as a convenience for chaining) */ public T initInstance(T instance, ColumnList<String> columns) { for (com.netflix.astyanax.model.Column<String> column : columns) { Field field = fields.get(column.getName()); if (field != null) { // otherwise it may be a column that was // removed, etc. Coercions.setFieldFromColumn(instance, field, column); } } return instance; } /** * Load a set of rows into new instances populated with values from the * column lists * * @param rows * the rows * @return list of new instances * @throws IllegalAccessException * if a new instance could not be instantiated * @throws InstantiationException * if a new instance could not be instantiated */ public List<T> getAll(Rows<?, String> rows) throws InstantiationException, IllegalAccessException { List<T> list = Lists.newArrayList(); for (Row<?, String> row : rows) { if (!row.getColumns().isEmpty()) { list.add(newInstance(row.getColumns())); } } return list; } /** * Return the set of column names discovered from the bean class * * @return column names */ public Collection<String> getNames() { return fields.keySet(); } Class<?> getIdFieldClass() { return fields.get(idFieldName).getType(); } private <ID extends Annotation, COLUMN extends Annotation> String mapField( Field field, AnnotationSet<ID, COLUMN> annotationSet, ImmutableMap.Builder<String, Field> builder, Set<String> usedNames, AtomicBoolean isKey) { String mappingName = null; ID idAnnotation = field.getAnnotation(annotationSet.getIdAnnotation()); COLUMN columnAnnotation = field.getAnnotation(annotationSet .getColumnAnnotation()); if ((idAnnotation != null) && (columnAnnotation != null)) { throw new IllegalStateException( "A field cannot be marked as both an ID and a Column: " + field.getName()); } if (idAnnotation != null) { mappingName = annotationSet.getIdName(field, idAnnotation); isKey.set(true); } else { isKey.set(false); } if ((columnAnnotation != null)) { mappingName = annotationSet.getColumnName(field, columnAnnotation); } if (mappingName != null) { Preconditions.checkArgument( !usedNames.contains(mappingName.toLowerCase()), mappingName + " has already been used for this column family"); usedNames.add(mappingName.toLowerCase()); field.setAccessible(true); builder.put(mappingName, field); } return mappingName; } }