/* * 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<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 * * @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. * * @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. * * @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. * * @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); } /** * @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); } /** * @return the annotations found while mapping */ public Map<Class<? extends Annotation>, Annotation> getAnnotations() { return foundAnnotations; } /** * @return a constructor for the type represented by the field */ public Constructor getCTor() { return constructor; } /** * @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(); } /** * @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)); } /** * @return the declaring class of the java field */ public Class getDeclaringClass() { return field.getDeclaringClass(); } /** * @return the underlying java field */ public Field getField() { return 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 * * @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; } /** * @return the full name of the class plus java field name */ public String getFullName() { return field.getDeclaringClass().getName() + "." + field.getName(); } /** * @return the name of the java field, as declared on the class */ public String getJavaFieldName() { return field.getName(); } /** * @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<T,V> * * @return the type of the map key */ public Class getMapKeyClass() { return toClass(mapKeyType); } /** * @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>, 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>, T[], Map<?,T> * * @return the parameterized type of the field */ public Type getSubType() { return subType; } /** * @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; } /** * @return the type of the underlying java field */ public Class getType() { return realType; } /** * @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) * * @param ann the annotation to search for * @return true if the annotation was found */ public boolean hasAnnotation(final Class ann) { return foundAnnotations.containsKey(ann); } /** * @return true if the MappedField is an array */ public boolean isArray() { return isArray; } /** * @return true if the MappedField is a Map */ public boolean isMap() { return isMap; } /** * @return true if this field is a container type such as a List, Map, Set, or array */ public boolean isMultipleValues() { return !isSingleValue(); } /** * @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(); } /** * @return true if the MappedField is a Set */ public boolean isSet() { return isSet; } /** * @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; } /** * @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. * * @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 * * @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. */ 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; } /** * @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; } }