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.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.TimeUnit; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Serializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.feedly.cassandra.IIndexRowPartitioner; import com.feedly.cassandra.anno.Column; import com.feedly.cassandra.anno.ColumnFamily; import com.feedly.cassandra.anno.Index; import com.feedly.cassandra.anno.Indexes; import com.feedly.cassandra.anno.RowKey; import com.feedly.cassandra.anno.UnmappedColumnHandler; import com.feedly.cassandra.dao.CounterColumn; /** * This class holds metadata for a given entity including key, property and index information. * * @author kireet * * @param <V> the entity type */ public class EntityMetadata<V> extends EntityMetadataBase<V> { private static final Logger _logger = LoggerFactory.getLogger(EntityMetadata.class.getName()); private final Set<Annotation> _annotations; private final SimplePropertyMetadata _keyMeta; private final String _familyName; private final byte[] _familyNameBytes; private final String _idxFamilyName; private final String _counterFamilyName; private final List<IndexMetadata> _indexes; private final Map<SimplePropertyMetadata, List<IndexMetadata>> _indexesByProp; @SuppressWarnings("unchecked") public EntityMetadata(Class<V> clazz) { super(clazz, areCompositeColsForced(clazz), ttlValue(clazz), false); ColumnFamily familyAnno = clazz.getAnnotation(ColumnFamily.class); _familyName = familyAnno.name(); _familyNameBytes = StringSerializer.get().toBytes(_familyName); _idxFamilyName = familyAnno.name() + "_idx"; _counterFamilyName = familyAnno.name() + "_cntr"; Set<Annotation> beanAnnos = new HashSet<Annotation>(); for(Annotation a : clazz.getAnnotations()) beanAnnos.add(a); _annotations = Collections.unmodifiableSet(beanAnnos); List<IndexMetadata> indexes = new ArrayList<IndexMetadata>(); Map<SimplePropertyMetadata, List<IndexMetadata>> indexesByProp = new HashMap<SimplePropertyMetadata, List<IndexMetadata>>(); SimplePropertyMetadata keyMeta = null; for(Field f : clazz.getDeclaredFields()) { if(f.isAnnotationPresent(RowKey.class)) { Method getter = getGetter(f); Method setter = getSetter(f); if(keyMeta != null) throw new IllegalArgumentException("@RowKey may only be used on one field."); if(getter == null || setter == null) throw new IllegalArgumentException("@RowKey field must have valid getter and setter."); if(!PropertyMetadataFactory.isSimpleType(f)) throw new IllegalArgumentException("@RowKey may only be used on a simple type, not custom types or collections."); RowKey anno = f.getAnnotation(RowKey.class); keyMeta = PropertyMetadataFactory.buildSimplePropertyMetadata(f, null, -1, getter, setter, (Class<? extends Serializer<?>>) anno.value(), false); } if(f.isAnnotationPresent(Column.class)) { 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"); PropertyMetadataBase pm = getProperty(f.getName()); if(anno.hashIndexed() || anno.rangeIndexed()) { if(pm.getPropertyType() != EPropertyType.SIMPLE) throw new IllegalStateException(f.getName() + ": property cannot be indexed, not a simple type: " + pm.getPropertyType()); if(anno.hashIndexed() && anno.rangeIndexed()) throw new IllegalArgumentException(f.getName() + ": cannot be both hash and range indexed, select one or the other."); IndexMetadata idxMeta = new IndexMetadata(familyAnno.name(), Collections.singletonList( (SimplePropertyMetadata) pm), createPartitioner(anno.rangeIndexPartitioner()), anno.hashIndexed() ? EIndexType.HASH : EIndexType.RANGE); indexes.add(idxMeta); List<IndexMetadata> l = indexesByProp.get(pm); if(l == null) { l = new ArrayList<IndexMetadata>(); indexesByProp.put((SimplePropertyMetadata) pm, l); } l.add(idxMeta); } if(f.isAnnotationPresent(UnmappedColumnHandler.class)) { throw new IllegalArgumentException(f.getName() + ": @UnmappedColumnHandler should not also be annotated as a mapped @Column."); } } } if(keyMeta == null) throw new IllegalArgumentException("missing @RowKey annotated field"); if(!getAnnotatedProperties(RowKey.class).isEmpty()) _logger.warn(keyMeta.getName(), ": key property is also stored in a column"); _keyMeta = keyMeta; /* * include indexes declared at the class level. Usually these are indexes that are on multiple columns */ Index idxAnno = clazz.getAnnotation(Index.class); Indexes idxArrAnno = clazz.getAnnotation(Indexes.class); Index[] allIdxAnnos; if(idxAnno == null) { allIdxAnnos = idxArrAnno == null ? new Index[0] : idxArrAnno.value(); } else { if(idxArrAnno == null) allIdxAnnos = new Index[] {idxAnno}; else { allIdxAnnos = new Index[idxArrAnno.value().length+1]; System.arraycopy(idxArrAnno.value(), 0, allIdxAnnos, 0, idxArrAnno.value().length); allIdxAnnos[allIdxAnnos.length-1] = idxAnno; } } for(Index anno : allIdxAnnos) { List<SimplePropertyMetadata> l = new ArrayList<SimplePropertyMetadata>(); if(anno.props() == null || anno.props().length == 0) throw new IllegalStateException("no properties referenced in index: "); for(String prop : anno.props()) { PropertyMetadataBase p = getProperty(prop); if(prop == null) throw new IllegalStateException("unrecognized property referenced in index: " + prop); if(l.contains(p)) throw new IllegalStateException("duplicate property referenced in index: " + prop); if(p == null || p.getPropertyType() != EPropertyType.SIMPLE) throw new IllegalStateException("non existent, non primitive or enum property referenced in index: " + prop); l.add((SimplePropertyMetadata) p); } IndexMetadata im = new IndexMetadata(familyAnno.name(), l, createPartitioner(anno.partitioner()), EIndexType.RANGE); indexes.add(im); for(SimplePropertyMetadata p : l) { List<IndexMetadata> pl = indexesByProp.get(p); if(pl == null) { pl = new ArrayList<IndexMetadata>(); indexesByProp.put(p, pl); } pl.add(im); } } Set<String> indexIds = new HashSet<String>(); for(IndexMetadata m : indexes) { if(!indexIds.add(m.id())) throw new IllegalStateException("duplicate index " + m); } _indexes = Collections.unmodifiableList(indexes); for(Entry<SimplePropertyMetadata, List<IndexMetadata>> entry : indexesByProp.entrySet()) entry.setValue(Collections.unmodifiableList(entry.getValue())); _indexesByProp = indexesByProp; } private static int ttlValue(Class<?> clazz) { ColumnFamily familyAnno = clazz.getAnnotation(ColumnFamily.class); if(familyAnno == null) throw new NullPointerException("@ColumnFamily annotation not set."); if(familyAnno.ttl() < 0) return -1; int ttl = (int) TimeUnit.SECONDS.convert(familyAnno.ttl(), familyAnno.ttlUnit()); if(ttl == 0) throw new IllegalArgumentException("TTL must be positive (>= 1 second) or unset"); return ttl; } private static boolean areCompositeColsForced(Class<?> clazz) { ColumnFamily familyAnno = clazz.getAnnotation(ColumnFamily.class); if(familyAnno == null) throw new NullPointerException("@ColumnFamily annotation not set."); return familyAnno.forceCompositeColumns() || !onlySimpleTypes(clazz); } private static boolean onlySimpleTypes(Class<?> clazz) { for(Field f : clazz.getDeclaredFields()) { if(f.isAnnotationPresent(Column.class) && !PropertyMetadataFactory.isSimpleType(f) && !f.getType().equals(CounterColumn.class)) return false; } return true; } private IIndexRowPartitioner createPartitioner(Class<? extends IIndexRowPartitioner> clazz) { try { return clazz.newInstance(); } catch(Exception ex) { throw new IllegalStateException("no no-arg constructor for partitioner " + clazz.getName()); } } public Set<Annotation> getAnnotations() { return _annotations; } public List<IndexMetadata> getIndexes() { return _indexes; } public boolean isIndexed(SimplePropertyMetadata pm) { return _indexesByProp.containsKey(pm); } public List<IndexMetadata> getIndexes(SimplePropertyMetadata pm) { List<IndexMetadata> l = _indexesByProp.get(pm); return l == null ? Collections.<IndexMetadata>emptyList() : l; } public SimplePropertyMetadata getKeyMetadata() { return _keyMeta; } public String getFamilyName() { return _familyName; } public byte[] getFamilyNameBytes() { return _familyNameBytes; } public String getIndexFamilyName() { return _idxFamilyName; } public String getCounterFamilyName() { return _counterFamilyName; } }