package com.feedly.cassandra.entity; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.TimeUnit; import me.prettyprint.hector.api.Serializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.feedly.cassandra.anno.Column; import com.feedly.cassandra.anno.UnmappedColumnHandler; /** * This class holds metadata for a given entity including key, property and index information. * * @author kireet * * @param <V> the entity type */ public class EntityMetadataBase<V> { private static final Logger _logger = LoggerFactory.getLogger(EntityMetadataBase.class.getName()); private final Map<String, PropertyMetadataBase> _propsByName, _propsByPhysicalName; private final List<PropertyMetadataBase> _props; private final Map<String, Set<PropertyMetadataBase>> _propsByAnno; private final Class<V> _clazz; private final Map<PropertyMetadataBase, Integer> _propPositions; private final boolean _useCompositeColumns; private final MapPropertyMetadata _unmappedHandler; private final boolean _hasCounterCols, _hasNormalCols; @SuppressWarnings("unchecked") public EntityMetadataBase(Class<V> clazz, boolean useCompositeColumns, int ttl, boolean overrideTtl) { _clazz = clazz; _useCompositeColumns = useCompositeColumns; Map<String, PropertyMetadataBase> props = new TreeMap<String, PropertyMetadataBase>(); Map<String, PropertyMetadataBase> propsByPhysical = new TreeMap<String, PropertyMetadataBase>(); Map<String, Set<PropertyMetadataBase>> propsByAnno = new TreeMap<String, Set<PropertyMetadataBase>>(); MapPropertyMetadata unmappedHandler = null; boolean hasCounters = false, hasNormalColumns = false; for(Field f : clazz.getDeclaredFields()) { Method getter = getGetter(f); Method setter = getSetter(f); if(f.isAnnotationPresent(UnmappedColumnHandler.class)) { if(unmappedHandler != null) throw new IllegalArgumentException("@UnmappedColumnHandler may only be used on one field."); if(!Map.class.equals(f.getType()) && !SortedMap.class.equals(f.getType())) throw new IllegalArgumentException("@UnmappedColumnHandler may only be used on a Map or SortedMap, not sub-interfaces or classes."); if(getter == null || setter == null) throw new IllegalArgumentException("@UnmappedColumnHandler field must have valid getter and setter."); UnmappedColumnHandler anno = f.getAnnotation(UnmappedColumnHandler.class); unmappedHandler = (MapPropertyMetadata) PropertyMetadataFactory.buildPropertyMetadata(f, null, -1, getter, setter, (Class<? extends Serializer<?>>) anno.value(), useCompositeColumns()); } if(f.isAnnotationPresent(Column.class)) { if(getter == null || setter == null) throw new IllegalArgumentException("@Column field must have valid getter and setter."); Column anno = f.getAnnotation(Column.class); String col = anno.name(); if(col.equals("")) col = f.getName(); if(anno.hashIndexed() && anno.rangeIndexed()) throw new IllegalStateException(f.getName() + ": property can be range or hash indexed, not both"); int colTtl = anno.ttl() > 0 ? (int) TimeUnit.SECONDS.convert(anno.ttl(), anno.ttlUnit()) : -1; if(overrideTtl && ttl > 0) //if override and entity level ttl is set colTtl = ttl; else if(!overrideTtl && colTtl < 0) //if not overriding and prop level ttl is not set colTtl = ttl; PropertyMetadataBase pm = PropertyMetadataFactory.buildPropertyMetadata(f, col, colTtl, getter, setter, (Class<? extends Serializer<?>>) anno.serializer(), useCompositeColumns); hasCounters = hasCounters || pm.hasCounter(); hasNormalColumns = hasNormalColumns || pm.hasSimple(); props.put(f.getName(), pm); if(propsByPhysical.put(col, pm) != null) throw new IllegalStateException(f.getName() + ": physical column name must be unique - " + col); for(Annotation a : f.getDeclaredAnnotations()) { Set<PropertyMetadataBase> annos = propsByAnno.get(a.annotationType().getName()); if(annos == null) { annos = new TreeSet<PropertyMetadataBase>(); propsByAnno.put(a.annotationType().getName(), annos); } annos.add(pm); } } } _hasCounterCols = hasCounters; _hasNormalCols = hasNormalColumns; _unmappedHandler = unmappedHandler; for(Entry<String, Set<PropertyMetadataBase>> annos : propsByAnno.entrySet()) annos.setValue(Collections.unmodifiableSet(annos.getValue())); _propsByAnno = Collections.unmodifiableMap(propsByAnno); _propsByName = Collections.unmodifiableMap(props); _propsByPhysicalName = Collections.unmodifiableMap(propsByPhysical); List<PropertyMetadataBase> sorted = new ArrayList<PropertyMetadataBase>(props.values()); Collections.sort(sorted); _props = Collections.unmodifiableList(sorted); Map<PropertyMetadataBase, Integer> positions = new HashMap<PropertyMetadataBase, Integer>(); for(int i = sorted.size() - 1; i >=0; i--) positions.put(sorted.get(i), i); _propPositions = Collections.unmodifiableMap(positions); } protected Method getSetter(Field prop) { String name = "set" + Character.toUpperCase(prop.getName().charAt(0)) + prop.getName().substring(1); Method setter = null; try { setter = _clazz.getMethod(name, prop.getType()); if(!EntityUtils.isValidSetter(setter)) return null; return setter; } catch(NoSuchMethodException ex) { if(!prop.getName().startsWith("__")) _logger.trace(prop.getName() + " no setter {} ({}). excluding", name, prop.getType().getSimpleName()); return null; } } protected Method getGetter(Field prop) { String name = "get" + Character.toUpperCase(prop.getName().charAt(0)) + prop.getName().substring(1); Method getter = null; try { getter = _clazz.getMethod(name); if(!getter.getReturnType().equals(prop.getType()) || !EntityUtils.isValidGetter(getter)) return null; return getter; } catch(NoSuchMethodException ex) { if(!prop.getName().startsWith("__")) _logger.trace(prop.getName() + "no getter {}({}). excluding", name, prop.getType().getSimpleName()); return null; } } public final PropertyMetadataBase getProperty(String name) { return _propsByName.get(name); } public final int getPropertyPosition(SimplePropertyMetadata pm) { return _propPositions.get(pm); } public final PropertyMetadataBase getPropertyByPhysicalName(String pname) { return _propsByPhysicalName.get(pname); } public final List<PropertyMetadataBase> getProperties() { return _props; } public final Set<PropertyMetadataBase> getAnnotatedProperties(Class<? extends Annotation> annoType) { Set<PropertyMetadataBase> rv = _propsByAnno.get(annoType.getName()); return rv != null ? rv : Collections.<PropertyMetadataBase>emptySet(); } public final Class<V> getType() { return _clazz; } public boolean useCompositeColumns() { return _useCompositeColumns; } public MapPropertyMetadata getUnmappedHandler() { return _unmappedHandler; } public boolean hasCounterColumns() { return _hasCounterCols; } public boolean hasNormalColumns() { return _hasNormalCols; } @Override public final int hashCode() { return _clazz.hashCode(); } @Override public final boolean equals(Object obj) { if(obj instanceof EntityMetadataBase<?>) return _clazz.equals(((EntityMetadataBase<?>) obj)._clazz); return false; } @Override public final String toString() { StringBuilder b = new StringBuilder(); b.append(_clazz.getSimpleName()); for(PropertyMetadataBase pm : _props) b.append("\n\t+ ").append(pm); return b.toString(); } }