MappedField.java :  » MongoDB » morphia » com » google » code » morphia » mapping » Java Open Source

Java Open Source » MongoDB » morphia 
morphia » com » google » code » morphia » mapping » MappedField.java
package com.google.code.morphia.mapping;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.code.morphia.annotations.AlsoLoad;
import com.google.code.morphia.annotations.ConstructorArgs;
import com.google.code.morphia.annotations.Embedded;
import com.google.code.morphia.annotations.Id;
import com.google.code.morphia.annotations.Indexed;
import com.google.code.morphia.annotations.NotSaved;
import com.google.code.morphia.annotations.Property;
import com.google.code.morphia.annotations.Reference;
import com.google.code.morphia.annotations.Serialized;
import com.google.code.morphia.annotations.Version;
import com.google.code.morphia.logging.Logr;
import com.google.code.morphia.logging.MorphiaLoggerFactory;
import com.google.code.morphia.utils.ReflectionUtils;
import com.mongodb.DBObject;

/**
 * Represents the mapping of this field to/from mongodb (name, list<annotation>)
 * 
 * @author Scott Hernandez
 */
@SuppressWarnings({"unchecked", "rawtypes"})
public class MappedField {
  private static final Logr log = MorphiaLoggerFactory.get(MappedField.class);
  // The Annotations to look for when reflecting on the field (stored in the mappingAnnotations)
  public static List<Class<? extends Annotation>> interestingAnnotations = new ArrayList<Class<? extends Annotation>>(Arrays.asList(
      Serialized.class, 
      Indexed.class, 
      Property.class, 
      Reference.class, 
      Embedded.class, 
      Id.class,
      Version.class, 
      ConstructorArgs.class, 
      AlsoLoad.class, 
      NotSaved.class));
  
  protected Class persistedClass;
  protected Field field; // the field :)
  protected Class realType; // the real type
  protected Constructor ctor; // the constructor for the type
  protected String name; // the name to store in mongodb {name:value}
  // Annotations that have been found relevant to mapping
  protected Map<Class<? extends Annotation>, Annotation> foundAnnotations = new HashMap<Class<? extends Annotation>, Annotation>();
  protected Type subType = null; // the type (T) for the Collection<T>/T[]/Map<?,T>
  protected Type mapKeyType = null; // the type (T) for the Map<T,?>
  protected boolean isSingleValue = true; // indicates the field is a single value
  protected boolean isMongoType = false; // indicated the type is a mongo compatible type (our version of value-type)
  protected boolean isMap = false; // indicated if it implements Map interface
  protected boolean isSet = false; // indicated if the collection is a set
  //for debugging
  protected boolean isArray = false; // indicated if it is an Array
  protected boolean isCollection = false; // indicated if the collection is a list)
  
  /** the constructor */
  MappedField(Field f, Class<?> clazz) {
    f.setAccessible(true);
    field = f;
    persistedClass = clazz;
    discover();
  }
  
  /** the constructor */
  protected MappedField(){}
  
  /** Discovers interesting (that we care about) things about the field. */
  protected void discover() {
    for (Class<? extends Annotation> clazz : interestingAnnotations)
      addAnnotation(clazz);
    
    name = getMappedFieldName();
    
    //type must be discovered before the constructor.
    realType = discoverType();
    ctor = discoverCTor();
    discoverMultivalued();
    
    // check the main type
    isMongoType = ReflectionUtils.isPropertyType(realType);
    
    // if the main type isn't supported by the Mongo, see if the subtype is.
    // works for T[], List<T>, Map<?, T>, where T is Long/String/etc.
    if (!isMongoType && subType != null)
      isMongoType = ReflectionUtils.isPropertyType(subType);
    
    if (!isMongoType && !isSingleValue && (subType == null || subType.equals(Object.class))) {
      if(log.isWarningEnabled())
        log.warning("The multi-valued field '"
              + getFullName()
              + "' is a possible heterogenous collection. It cannot be verified. Please declare a valid type to get rid of this warning. " + subType );
      isMongoType = true;
    }
  }

  private void discoverMultivalued() {
    if (  realType.isArray() || 
        Collection.class.isAssignableFrom(realType) || 
        Map.class.isAssignableFrom(realType)) {
      
      isSingleValue = false;
      
      isMap = Map.class.isAssignableFrom(realType);
      isSet = Set.class.isAssignableFrom(realType);
      //for debugging
      isCollection = Collection.class.isAssignableFrom(realType);
      isArray = realType.isArray();
      
      //for debugging with issue
      if (!isMap && !isSet && !isCollection && !isArray)
        throw new MappingException("type is not a map/set/collection/array : " + realType);
      
      // get the subtype T, T[]/List<T>/Map<?,T>; subtype of Long[], List<Long> is Long
      subType = (realType.isArray()) ? realType.getComponentType() : ReflectionUtils.getParameterizedType(field, (isMap) ? 1 : 0);

      if (isMap)
        mapKeyType = ReflectionUtils.getParameterizedType(field, 0);
    }
  }
  
  private Class discoverType() {
    Class type = field.getType();
    Type gType = field.getGenericType();
    TypeVariable<GenericDeclaration> tv = null;
    ParameterizedType pt = null;
    if (gType instanceof TypeVariable)
      tv = (TypeVariable<GenericDeclaration>) gType;
    else if (gType instanceof ParameterizedType)
      pt = (ParameterizedType) gType;
    
    if (tv != null) {
//      type = ReflectionUtils.getTypeArgument(persistedClass, tv);
      Class typeArgument = ReflectionUtils.getTypeArgument(persistedClass, tv);
      if(typeArgument != null)
        type = typeArgument;
    } else if (pt != null) {
      if(log.isDebugEnabled())
        log.debug("found instance of ParameterizedType : " + pt);
    }
    
    if (Object.class.equals(realType) && (tv != null || pt != null))
      if(log.isWarningEnabled())
        log.warning("Parameterized types are treated as untyped Objects. See field '" + field.getName() + "' on " + field.getDeclaringClass() );
    
    if (type == null)
      throw new MappingException("A type could not be found for " + this.field);
    
    return type;
  }
  
  private Constructor discoverCTor() {
    Constructor returnCtor = null;
    Class ctorType = null;
    // get the first annotation with a concreteClass that isn't Object.class
    for (Annotation an : foundAnnotations.values()) {
      try {
        Method m = an.getClass().getMethod("concreteClass");
        m.setAccessible(true);
        Object o = m.invoke(an);
        if (o != null && !(o.equals(Object.class))) {
          ctorType = (Class) o;
          break;
        }
      } catch (NoSuchMethodException e) {
        // do nothing
      } catch (IllegalArgumentException e) {
        if(log.isWarningEnabled())
          log.warning("There should not be an argument", e);
      } catch (Exception e) {
        if(log.isWarningEnabled())
          log.warning("", e);
      }
    }
    
    if (ctorType != null)
      try {
        returnCtor = ctorType.getDeclaredConstructor();
        returnCtor.setAccessible(true);
      } catch (NoSuchMethodException e) {
        if (!hasAnnotation(ConstructorArgs.class))
          if(log.isWarningEnabled())
            log.warning("No usable constructor for " + ctorType.getName(), e);
      }
    else {
      // see if we can create instances of the type used for declaration
      ctorType = getType();
      if (ctorType != null)
        try {
          returnCtor = ctorType.getDeclaredConstructor();
          returnCtor.setAccessible(true);
        } catch (NoSuchMethodException e) {
          // never mind.
        } catch (SecurityException e) {
          // never mind.
        }
    }
    return returnCtor;
  }
  
  /** Returns the name of the field's (key)name for mongodb */
  public String getNameToStore() {
    return name;
  }
  
  /** Returns the name of the field's (key)name for mongodb, in order of loading. */
  public List<String> getLoadNames() {
    ArrayList<String> names = new ArrayList<String>();
    names.add(name);
    
    AlsoLoad al = (AlsoLoad)this.foundAnnotations.get(AlsoLoad.class);
    if (al != null && al.value() != null && al.value().length > 0)
      names.addAll( Arrays.asList(al.value()));
    
    return names;
  }
  
  /** @return the value of this field mapped from the DBObject */
  public String getFirstFieldName(DBObject dbObj) {
    String fieldName = getNameToStore();
    boolean foundField = false;
    for (String n : getLoadNames()) {
      if (dbObj.containsField(n))
        if (!foundField) {
          foundField = true;
          fieldName = n;
        } else
          throw new MappingException(String.format("Found more than one field from @AlsoLoad %s", getLoadNames()));
    }
    return fieldName;
  }
  
  /** @return the value from best mapping of this field*/
  public Object getDbObjectValue(DBObject dbObj) {
    return dbObj.get(getFirstFieldName(dbObj));
  }
  
  /** Returns the name of the java field, as declared on the class */
  public String getJavaFieldName() {
    return field.getName();
  }
  
  /** returns the annotation instance if it exists on this field*/
  public <T extends Annotation> T getAnnotation(Class<T> clazz) {
    return (T) foundAnnotations.get(clazz);
  }
  
  /** Indicates whether the annotation is present in the mapping (does not check the java field annotations, just the ones discovered) */
  public boolean hasAnnotation(Class ann) {
    return foundAnnotations.containsKey(ann);
  }
  
  /** Adds the annotation, if it exists on the field. */
  public void addAnnotation(Class<? extends Annotation> clazz) {
    if (field.isAnnotationPresent(clazz))
      this.foundAnnotations.put(clazz, field.getAnnotation(clazz));
  }

  /** returns the full name of the class plus java field name */
  public String getFullName() {
    return field.getDeclaringClass().getName() + "." + field.getName();
  }
  
  /**
   * Returns the name of the field's key-name for mongodb
   */
  private String getMappedFieldName() {
    if (hasAnnotation(Id.class))
      return Mapper.ID_KEY;
    else if (hasAnnotation(Property.class)) {
      Property mv = (Property) foundAnnotations.get(Property.class);
      if (!mv.value().equals(Mapper.IGNORED_FIELDNAME))
        return mv.value();
    } else if (hasAnnotation(Reference.class)) {
      Reference mr = (Reference) foundAnnotations.get(Reference.class);
      if (!mr.value().equals(Mapper.IGNORED_FIELDNAME))
        return mr.value();
    } else if (hasAnnotation(Embedded.class)) {
      Embedded me = (Embedded) foundAnnotations.get(Embedded.class);
      if (!me.value().equals(Mapper.IGNORED_FIELDNAME))
        return me.value();
    } else if (hasAnnotation(Serialized.class)) {
      Serialized me = (Serialized) foundAnnotations.get(Serialized.class);
      if (!me.value().equals(Mapper.IGNORED_FIELDNAME))
        return me.value();
    } else if (hasAnnotation(Version.class)) {
      Version me = (Version) foundAnnotations.get(Version.class);
      if (!me.value().equals(Mapper.IGNORED_FIELDNAME))
        return me.value();
    }
    
    return this.field.getName();
  }
  
  @Override
  public String toString() {
    StringBuffer sb = new StringBuffer();
    sb.append(name).append(" (");
    sb.append(" type:").append(realType.getSimpleName()).append(",");
    
    if(isSingleValue())
      sb.append(" single:true,");
    else {
      sb.append(" multiple:true,");
      sb.append(" subtype:").append(getSubClass()).append(",");
    }
    if(isMap()) {
      sb.append(" map:true,");
      if (getMapKeyClass() != null)
        sb.append(" map-key:").append(getMapKeyClass().getSimpleName());
      else
        sb.append(" map-key: class unknown! ");
    }
    
    if(isSet())
      sb.append(" set:true,");
    if(isCollection)
      sb.append(" collection:true,");
    if(isArray)
      sb.append(" array:true,");
    
    //remove last comma
    if (sb.charAt(sb.length()-1) == ',')
      sb.setLength(sb.length()-1);
    
    sb.append("); ").append(this.foundAnnotations.toString());
    return sb.toString();
  }
  
  /** returns the type of the underlying java field*/
  public Class getType() {
    return realType;
  }
  
  /** returns the declaring class of the java field */
  public Class getDeclaringClass() {
    return field.getDeclaringClass();
  }

  /** If the underlying java type is a map then it returns T from Map<T,V> */
  public Class getMapKeyClass() {
    return toClass(mapKeyType);
  }

  protected Class toClass(Type t) {
    if(t == null) 
      return null;
    else if(t instanceof Class)
      return (Class) t;
    else if(t instanceof ParameterizedType)
      return (Class)((ParameterizedType) t).getRawType();
    else if(t instanceof WildcardType)
      return (Class)((WildcardType) t).getUpperBounds()[0];

    throw new RuntimeException("Generic TypeVariable not supported!");
    
  }
  /** If the java field is a list/array/map then the sub-type T is returned (ex. List<T>, T[], Map<?,T>*/
  public Class getSubClass() {
    return toClass(subType);
  }
  
  public Type getSubType() {
    return subType;
  }
  
  public boolean isSingleValue() {
    if(!isSingleValue && !isMap && !isSet && !isArray && !isCollection)
      throw new RuntimeException("Not single, but none of the types that are not-single.");
    return isSingleValue;
  }
  
  public boolean isMultipleValues() {
    return !isSingleValue;
  }
  
  public boolean isTypeMongoCompatible() {
    return isMongoType;
  }
  
  public boolean isMap() {
    return isMap;
  }
  
  public boolean isSet() {
    return isSet;
  }
  /** returns a constructor for the type represented by the field */
  public Constructor getCTor() {
    return ctor;
  }

  /** Returns the value stored in the java field */
  public Object getFieldValue(Object classInst) throws IllegalArgumentException {
    try {
      field.setAccessible(true);
      return field.get(classInst);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }
  
  /** Sets the value for the java field */  
  public void setFieldValue(Object classInst, Object value) throws IllegalArgumentException {
    try {
      field.setAccessible(true);
      field.set(classInst, value);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }
  
  /** returned the underlying java field */
  public Field getField() {
    return field;
  }
  
  public Class getConcreteType() {
    Embedded e = getAnnotation(Embedded.class);
    if (e != null) {
      Class concrete = e.concreteClass();
      if (concrete != Object.class) {
        return concrete;
      }
    }
    
    Property p = getAnnotation(Property.class);
    if (p != null) {
      Class concrete = p.concreteClass();
      if (concrete != Object.class) {
        return concrete;
      }
    }
    return getType();
  }
}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.