/*
 * Copyright 2008-2016 MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mongodb.morphia.mapping;


import com.mongodb.DBObject;
import com.mongodb.DBRef;
import org.mongodb.morphia.Key;
import org.mongodb.morphia.annotations.AlsoLoad;
import org.mongodb.morphia.annotations.ConstructorArgs;
import org.mongodb.morphia.annotations.Embedded;
import org.mongodb.morphia.annotations.Id;
import org.mongodb.morphia.annotations.Indexed;
import org.mongodb.morphia.annotations.NotSaved;
import org.mongodb.morphia.annotations.Property;
import org.mongodb.morphia.annotations.Reference;
import org.mongodb.morphia.annotations.Serialized;
import org.mongodb.morphia.annotations.Text;
import org.mongodb.morphia.annotations.Transient;
import org.mongodb.morphia.annotations.Version;
import org.mongodb.morphia.logging.Logger;
import org.mongodb.morphia.logging.MorphiaLoggerFactory;
import org.mongodb.morphia.utils.ReflectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
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.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.lang.String.format;
import static java.util.Arrays.asList;


Represents the mapping of this field to/from mongodb (name, list)
Author:Scott Hernandez
/** * Represents the mapping of this field to/from mongodb (name, list<annotation>) * * @author Scott Hernandez */
@SuppressWarnings("unchecked") public class MappedField { private static final Logger LOG = MorphiaLoggerFactory.get(MappedField.class); // The Annotations to look for when reflecting on the field (stored in the mappingAnnotations) private static final List<Class<? extends Annotation>> INTERESTING = new ArrayList<Class<? extends Annotation>>(); static { INTERESTING.add(Serialized.class); INTERESTING.add(Indexed.class); INTERESTING.add(Property.class); INTERESTING.add(Reference.class); INTERESTING.add(Embedded.class); INTERESTING.add(Id.class); INTERESTING.add(Version.class); INTERESTING.add(ConstructorArgs.class); INTERESTING.add(AlsoLoad.class); INTERESTING.add(NotSaved.class); INTERESTING.add(Text.class); } // Annotations that have been found relevant to mapping private final Map<Class<? extends Annotation>, Annotation> foundAnnotations = new HashMap<Class<? extends Annotation>, Annotation>(); private final List<MappedField> typeParameters = new ArrayList<MappedField>(); private Class persistedClass; private Field field; // the field :) private Class realType; // the real type private Constructor constructor; // the constructor for the type private Type subType; // the type (T) for the Collection<T>/T[]/Map<?,T> private Type mapKeyType; // the type (T) for the Map<T,?> private boolean isSingleValue = true; // indicates the field is a single value private boolean isMongoType; // indicated the type is a mongo compatible type (our version of value-type) private boolean isMap; // indicated if it implements Map interface private boolean isSet; // indicated if the collection is a set //for debugging private boolean isArray; // indicated if it is an Array private boolean isCollection; // indicated if the collection is a list) private Type genericType; MappedField(final Field f, final Class<?> clazz, final Mapper mapper) { f.setAccessible(true); field = f; persistedClass = clazz; realType = field.getType(); genericType = field.getGenericType(); discover(mapper); }
Creates a MappedField
Params:
  • field – the Type for the field
  • type – the Type for the field
  • mapper – the Mapper to use
/** * Creates a MappedField * * @param field the Type for the field * @param type the Type for the field * @param mapper the Mapper to use */
MappedField(final Field field, final Type type, final Mapper mapper) { this.field = field; genericType = type; discoverType(mapper); }
Adds an annotation for Morphia to retain when mapping.
Params:
  • annotation – the type to retain
/** * Adds an annotation for Morphia to retain when mapping. * * @param annotation the type to retain */
public static void addInterestingAnnotation(final Class<? extends Annotation> annotation) { INTERESTING.add(annotation); }
Adds the annotation, if it exists on the field.
Params:
  • clazz – the annotation to add
/** * Adds the annotation, if it exists on the field. * * @param clazz the annotation to add */
public void addAnnotation(final Class<? extends Annotation> clazz) { if (field.isAnnotationPresent(clazz)) { foundAnnotations.put(clazz, field.getAnnotation(clazz)); } }
Adds the annotation, if it exists on the field.
Params:
  • clazz – type of the annotation
  • ann – the annotation
/** * Adds the annotation, if it exists on the field. * * @param clazz type of the annotation * @param ann the annotation */
public void addAnnotation(final Class<? extends Annotation> clazz, final Annotation ann) { foundAnnotations.put(clazz, ann); }
Params:
  • clazz – the annotation to search for
Type parameters:
  • <T> – the type of the annotation
Returns:the annotation instance if it exists on this field
/** * @param clazz the annotation to search for * @param <T> the type of the annotation * @return the annotation instance if it exists on this field */
@SuppressWarnings("unchecked") public <T extends Annotation> T getAnnotation(final Class<T> clazz) { return (T) foundAnnotations.get(clazz); }
Returns:the annotations found while mapping
/** * @return the annotations found while mapping */
public Map<Class<? extends Annotation>, Annotation> getAnnotations() { return foundAnnotations; }
Returns:a constructor for the type represented by the field
/** * @return a constructor for the type represented by the field */
public Constructor getCTor() { return constructor; }
Returns:the concrete type of the MappedField
/** * @return the concrete type of the MappedField */
public Class getConcreteType() { final Embedded e = getAnnotation(Embedded.class); if (e != null) { final Class concrete = e.concreteClass(); if (concrete != Object.class) { return concrete; } } final Property p = getAnnotation(Property.class); if (p != null) { final Class concrete = p.concreteClass(); if (concrete != Object.class) { return concrete; } } return getType(); }
Params:
  • dbObj – the DBObject get the value from
Returns:the value from best mapping of this field
/** * @param dbObj the DBObject get the value from * @return the value from best mapping of this field */
public Object getDbObjectValue(final DBObject dbObj) { return dbObj.get(getFirstFieldName(dbObj)); }
Returns:the declaring class of the java field
/** * @return the declaring class of the java field */
public Class getDeclaringClass() { return field.getDeclaringClass(); }
Returns:the underlying java field
/** * @return the underlying java field */
public Field getField() { return field; }
Gets the value of the field mapped on the instance given.
Params:
  • instance – the instance to use
Returns:the value stored in the java field
/** * Gets the value of the field mapped on the instance given. * * @param instance the instance to use * @return the value stored in the java field */
public Object getFieldValue(final Object instance) { try { return field.get(instance); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }
Gets the field name to use when converting from a DBObject
Params:
  • dbObj – the DBObject to scan for alternate names
See Also:
Returns:the value of this field mapped from the DBObject
/** * Gets the field name to use when converting from a DBObject * * @param dbObj the DBObject to scan for alternate names * @return the value of this field mapped from the DBObject * @see AlsoLoad */
public String getFirstFieldName(final DBObject dbObj) { String fieldName = getNameToStore(); boolean foundField = false; for (final String n : getLoadNames()) { if (dbObj.containsField(n)) { if (!foundField) { foundField = true; fieldName = n; } else { throw new MappingException(format("Found more than one field from @AlsoLoad %s", getLoadNames())); } } } return fieldName; }
Returns:the full name of the class plus java field name
/** * @return the full name of the class plus java field name */
public String getFullName() { return field.getDeclaringClass().getName() + "." + field.getName(); }
Returns:the name of the java field, as declared on the class
/** * @return the name of the java field, as declared on the class */
public String getJavaFieldName() { return field.getName(); }
Returns:the name of the field's (key)name for mongodb, in order of loading.
/** * @return the name of the field's (key)name for mongodb, in order of loading. */
public List<String> getLoadNames() { final List<String> names = new ArrayList<String>(); names.add(getMappedFieldName()); final AlsoLoad al = (AlsoLoad) foundAnnotations.get(AlsoLoad.class); if (al != null && al.value() != null && al.value().length > 0) { names.addAll(asList(al.value())); } return names; }
If the underlying java type is a map then it returns T from Map
Returns:the type of the map key
/** * If the underlying java type is a map then it returns T from Map<T,V> * * @return the type of the map key */
public Class getMapKeyClass() { return toClass(mapKeyType); }
Returns:the name of the field's (key)name for mongodb
/** * @return the name of the field's (key)name for mongodb */
public String getNameToStore() { return getMappedFieldName(); }
If the java field is a list/array/map then the sub-type T is returned (ex. List, T[], Map
Returns:the parameterized type of the field
/** * If the java field is a list/array/map then the sub-type T is returned (ex. List<T>, T[], Map<?,T> * * @return the parameterized type of the field */
public Class getSubClass() { return toClass(subType); }
If the java field is a list/array/map then the sub-type T is returned (ex. List, T[], Map
Returns:the parameterized type of the field
/** * If the java field is a list/array/map then the sub-type T is returned (ex. List<T>, T[], Map<?,T> * * @return the parameterized type of the field */
public Type getSubType() { return subType; }
Returns:true if this field is marked as transient
/** * @return true if this field is marked as transient */
public boolean isTransient() { return hasAnnotation(Transient.class) || Modifier.isTransient(field.getModifiers()); } void setSubType(final Type subType) { this.subType = subType; }
Returns:the type of the underlying java field
/** * @return the type of the underlying java field */
public Class getType() { return realType; }
Returns:the type parameters defined on the field
/** * @return the type parameters defined on the field */
public List<MappedField> getTypeParameters() { return typeParameters; }
Indicates whether the annotation is present in the mapping (does not check the java field annotations, just the ones discovered)
Params:
  • ann – the annotation to search for
Returns:true if the annotation was found
/** * Indicates whether the annotation is present in the mapping (does not check the java field annotations, just the ones discovered) * * @param ann the annotation to search for * @return true if the annotation was found */
public boolean hasAnnotation(final Class ann) { return foundAnnotations.containsKey(ann); }
Returns:true if the MappedField is an array
/** * @return true if the MappedField is an array */
public boolean isArray() { return isArray; }
Returns:true if the MappedField is a Map
/** * @return true if the MappedField is a Map */
public boolean isMap() { return isMap; }
Returns:true if this field is a container type such as a List, Map, Set, or array
/** * @return true if this field is a container type such as a List, Map, Set, or array */
public boolean isMultipleValues() { return !isSingleValue(); }
See Also:
Returns:true if this field is a reference to a foreign document
/** * @return true if this field is a reference to a foreign document * @see Reference * @see Key * @see DBRef */
public boolean isReference() { return hasAnnotation(Reference.class) || Key.class == getConcreteType() || DBRef.class == getConcreteType(); }
Returns:true if the MappedField is a Set
/** * @return true if the MappedField is a Set */
public boolean isSet() { return isSet; }
Returns:true if this field is not a container type such as a List, Map, Set, or array
/** * @return true if this field is not a container type such as a List, Map, Set, or array */
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; }
Returns:true if type is understood by MongoDB and the driver
/** * @return true if type is understood by MongoDB and the driver */
public boolean isTypeMongoCompatible() { return isMongoType; }
Adds the annotation even if not on the declared class/field.
Params:
  • ann – the annotation to add
Returns:ann the annotation
/** * Adds the annotation even if not on the declared class/field. * * @param ann the annotation to add * @return ann the annotation */
public Annotation putAnnotation(final Annotation ann) { return foundAnnotations.put(ann.getClass(), ann); }
Sets the value for the java field
Params:
  • instance – the instance to update
  • value – the value to set
/** * Sets the value for the java field * * @param instance the instance to update * @param value the value to set */
public void setFieldValue(final Object instance, final Object value) { try { field.set(instance, value); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(getMappedFieldName()).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(foundAnnotations.toString()); return sb.toString(); }
Discovers interesting (that we care about) things about the field.
/** * Discovers interesting (that we care about) things about the field. */
protected void discover(final Mapper mapper) { for (final Class<? extends Annotation> clazz : INTERESTING) { addAnnotation(clazz); } //type must be discovered before the constructor. discoverType(mapper); constructor = discoverConstructor(); 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 == Object.class)) { if (LOG.isWarningEnabled() && !mapper.getConverters().hasDbObjectConverter(this)) { LOG.warning(format("The multi-valued field '%s' is a possible heterogeneous collection. It cannot be verified. " + "Please declare a valid type to get rid of this warning. %s", getFullName(), subType)); } isMongoType = true; } } @SuppressWarnings("unchecked") protected void discoverType(final Mapper mapper) { if (genericType instanceof TypeVariable) { realType = extractTypeVariable((TypeVariable) genericType); } else if (genericType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) genericType; final Type[] types = pt.getActualTypeArguments(); realType = toClass(pt); for (Type type : types) { if (type instanceof ParameterizedType) { typeParameters.add(new EphemeralMappedField((ParameterizedType) type, this, mapper)); } else { if (type instanceof WildcardType) { type = ((WildcardType) type).getUpperBounds()[0]; } typeParameters.add(new EphemeralMappedField(type, this, mapper)); } } } else if (genericType instanceof WildcardType) { final WildcardType wildcardType = (WildcardType) genericType; final Type[] types = wildcardType.getUpperBounds(); realType = toClass(types[0]); } else if (genericType instanceof Class) { realType = (Class) genericType; } else if (genericType instanceof GenericArrayType) { final Type genericComponentType = ((GenericArrayType) genericType).getGenericComponentType(); if (genericComponentType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) genericComponentType; realType = toClass(genericType); final Type[] types = pt.getActualTypeArguments(); for (Type type : types) { if (type instanceof ParameterizedType) { typeParameters.add(new EphemeralMappedField((ParameterizedType) type, this, mapper)); } else { if (type instanceof WildcardType) { type = ((WildcardType) type).getUpperBounds()[0]; } typeParameters.add(new EphemeralMappedField(type, this, mapper)); } } } else { if (genericComponentType instanceof TypeVariable) { realType = toClass(genericType); } else { realType = (Class) genericComponentType; } } } if (Object.class.equals(realType) || Object[].class.equals(realType)) { if (LOG.isWarningEnabled()) { LOG.warning(format("Parameterized types are treated as untyped Objects. See field '%s' on %s", field.getName(), field.getDeclaringClass())); } } if (realType == null) { throw new MappingException(format("A type could not be found for the field %s.%s", getType(), getField())); } } private Class extractTypeVariable(final TypeVariable<?> type) { final Class typeArgument = ReflectionUtils.getTypeArgument(persistedClass, type); return typeArgument != null ? typeArgument : Object.class; }
Returns:the name of the field's key-name for mongodb
/** * @return the name of the field's key-name for mongodb */
protected String getMappedFieldName() { if (hasAnnotation(Id.class)) { return Mapper.ID_KEY; } else if (hasAnnotation(Property.class)) { final Property mv = (Property) foundAnnotations.get(Property.class); if (!mv.value().equals(Mapper.IGNORED_FIELDNAME)) { return mv.value(); } } else if (hasAnnotation(Reference.class)) { final Reference mr = (Reference) foundAnnotations.get(Reference.class); if (!mr.value().equals(Mapper.IGNORED_FIELDNAME)) { return mr.value(); } } else if (hasAnnotation(Embedded.class)) { final Embedded me = (Embedded) foundAnnotations.get(Embedded.class); if (!me.value().equals(Mapper.IGNORED_FIELDNAME)) { return me.value(); } } else if (hasAnnotation(Serialized.class)) { final Serialized me = (Serialized) foundAnnotations.get(Serialized.class); if (!me.value().equals(Mapper.IGNORED_FIELDNAME)) { return me.value(); } } else if (hasAnnotation(Version.class)) { final Version me = (Version) foundAnnotations.get(Version.class); if (!me.value().equals(Mapper.IGNORED_FIELDNAME)) { return me.value(); } } return field.getName(); } protected Class toClass(final Type t) { if (t == null) { return null; } else if (t instanceof Class) { return (Class) t; } else if (t instanceof GenericArrayType) { final Type type = ((GenericArrayType) t).getGenericComponentType(); Class aClass; if (type instanceof ParameterizedType) { aClass = (Class) ((ParameterizedType) type).getRawType(); } else if (type instanceof TypeVariable) { aClass = ReflectionUtils.getTypeArgument(persistedClass, (TypeVariable<?>) type); if (aClass == null) { aClass = Object.class; } } else { aClass = (Class) type; } return Array.newInstance(aClass, 0).getClass(); } 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!"); } private Constructor discoverConstructor() { Class<?> type = null; // get the first annotation with a concreteClass that isn't Object.class for (final Annotation an : foundAnnotations.values()) { try { final Method m = an.getClass().getMethod("concreteClass"); m.setAccessible(true); final Object o = m.invoke(an); //noinspection EqualsBetweenInconvertibleTypes if (o != null && !(o.equals(Object.class))) { type = (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 (type != null) { try { constructor = type.getDeclaredConstructor(); constructor.setAccessible(true); } catch (NoSuchMethodException e) { if (!hasAnnotation(ConstructorArgs.class)) { if (LOG.isWarningEnabled()) { LOG.warning("No usable constructor for " + type.getName(), e); } } } } else { // see if we can create instances of the type used for declaration type = getType(); // short circuit to avoid wasting time throwing an exception trying to get a constructor we know doesnt exist if (type == List.class || type == Map.class) { return null; } if (type != null) { try { constructor = type.getDeclaredConstructor(); constructor.setAccessible(true); } catch (NoSuchMethodException e) { // never mind. } catch (SecurityException e) { // never mind. } } } return constructor; } private void discoverMultivalued() { if (realType.isArray() || Collection.class.isAssignableFrom(realType) || Map.class.isAssignableFrom(realType) || GenericArrayType.class.isAssignableFrom(genericType.getClass())) { 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(format("%s.%s is not a map/set/collection/array : %s", field.getName(), field.getDeclaringClass(), 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); } } } void setIsMap(final boolean isMap) { this.isMap = isMap; } void setIsMongoType(final boolean isMongoType) { this.isMongoType = isMongoType; } void setIsSet(final boolean isSet) { this.isSet = isSet; } void setMapKeyType(final Class mapKeyType) { this.mapKeyType = mapKeyType; } }