package meetup.beeno.mapping; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import meetup.beeno.HEntity; import meetup.beeno.HProperty; import meetup.beeno.HRowKey; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.HTablePool; import org.apache.log4j.Logger; /** * Central cache for parsed mapping metadata on entity classes. Given an entity class reference, * this will parse out {@link HEntity}. {@link HRowKey}, {@link HProperty} annotations for the class * and cache the results for future queries. * * @author garyh * */ public class EntityMetadata { private static Logger log = Logger.getLogger(EntityMetadata.class.getName()); private static EntityMetadata instance = null; public static enum PropertyType { string(String.class), int_type(Integer.TYPE), float_type(Float.TYPE), double_type(Double.TYPE), long_type(Long.TYPE); private Class clazz = null; PropertyType(Class clazz) { this.clazz = clazz; } public Class getTypeClass() { return this.clazz; } }; private Map<Class, EntityInfo> mappings = new ConcurrentHashMap<Class, EntityInfo>(); private HTablePool pool = new HTablePool(); private EntityMetadata() { } /** * Returns the annotated HBase metadata for the given entity class. If the class is not * yet parsed, it will be parsed and added to the internal mapping. * * @param entityClass * @return */ public EntityInfo getInfo(Class entityClass) throws MappingException { EntityInfo info = this.mappings.get(entityClass); if (info == null) { info = parseEntity(entityClass); mappings.put(entityClass, info); } return info; } /** * Reads all mapping annotations from the passed in class, to build up * a set of the HBase table and column associations. * * @param clazz * @return * @throws MappingException */ protected EntityInfo parseEntity(Class clazz) throws MappingException { // lookup any class mappings HEntity classTable = (HEntity) clazz.getAnnotation(HEntity.class); if (classTable == null) { throw new MappingException(clazz, "Not an entity class!"); } EntityInfo info = new EntityInfo(clazz); info.setTablename(classTable.name()); // lookup any property mappings for table fields and indexes parseProperties(clazz, info); // make sure we have a mapping for the row key if (info.getKeyProperty() == null) { throw new MappingException(clazz, "Missing required annotation for HTable row key property"); } return info; } /** * Examines the java bean properties for the class, looking for column mappings * and a mapping for the row key. * @param clazz * @param info */ protected void parseProperties(Class clazz, EntityInfo info) throws MappingException { try { BeanInfo clazzInfo = Introspector.getBeanInfo(clazz); for (PropertyDescriptor prop : clazzInfo.getPropertyDescriptors()) { Method readMethod = prop.getReadMethod(); if (readMethod != null) { parseMethod(prop, readMethod, info); } Method writeMethod = prop.getWriteMethod(); if (writeMethod != null) { parseMethod(prop, writeMethod, info); } } } catch (IntrospectionException ie) { log.error("Failed to get BeanInfo: "+ie.getMessage()); } } protected void parseMethod(PropertyDescriptor prop, Method meth, EntityInfo info) throws MappingException { // see if this is mapped to the row key -- if so it's not allowed to be a field HRowKey key = (HRowKey) meth.getAnnotation(HRowKey.class); if (key != null) { // check for a duplicate mapping (composite keys not supported) if (info.getKeyProperty() != null && !prop.equals(info.getKeyProperty())) { throw new MappingException( info.getEntityClass(), String.format("Duplicate mappings for table row key: %s, %s", info.getKeyProperty().getName(), prop.getName()) ); } info.setKeyProperty(prop); return; } // check for property mapping HProperty propAnnotation = (HProperty) meth.getAnnotation(HProperty.class); if (propAnnotation != null) { String fieldname = fieldToString(propAnnotation); PropertyDescriptor currentMapped = info.getFieldProperty(fieldname); // check for a duplicate mapping if (currentMapped != null && !prop.equals(currentMapped)) { throw new MappingException( info.getEntityClass(), String.format("Duplicate mappings for table field: %s, %s", currentMapped.getName(), prop.getName()) ); } String typeName = propAnnotation.type(); PropertyType type = null; if (typeName != null && !"".equals(typeName.trim())) { try { type = PropertyType.valueOf(typeName); } catch (IllegalArgumentException iae) { throw new MappingException( info.getEntityClass(), String.format("Invalid property type ('%s') for '%s'", typeName, prop.getName()) ); } } info.addProperty(propAnnotation, prop, type); } } protected String fieldToString(HProperty prop) { StringBuilder builder = new StringBuilder(prop.family()).append(":"); if (prop.name() != null && !"*".equals(prop.name())) builder.append(prop.name()); return builder.toString(); } /** * Returns the main metadata instance. Ignoring synchronization here * as having a few copies initially isn't too bad. * @return */ public static EntityMetadata getInstance() { if (instance == null) instance = new EntityMetadata(); return instance; } }