All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mongodb.morphia.mapping.MappedClass Maven / Gradle / Ivy

/*
 * 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 org.mongodb.morphia.EntityInterceptor;
import org.mongodb.morphia.annotations.Converters;
import org.mongodb.morphia.annotations.Embedded;
import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.EntityListeners;
import org.mongodb.morphia.annotations.Field;
import org.mongodb.morphia.annotations.Id;
import org.mongodb.morphia.annotations.IndexOptions;
import org.mongodb.morphia.annotations.Indexes;
import org.mongodb.morphia.annotations.PostLoad;
import org.mongodb.morphia.annotations.PostPersist;
import org.mongodb.morphia.annotations.PreLoad;
import org.mongodb.morphia.annotations.PrePersist;
import org.mongodb.morphia.annotations.PreSave;
import org.mongodb.morphia.annotations.Property;
import org.mongodb.morphia.annotations.Reference;
import org.mongodb.morphia.annotations.Serialized;
import org.mongodb.morphia.annotations.Transient;
import org.mongodb.morphia.annotations.Validation;
import org.mongodb.morphia.annotations.Version;
import org.mongodb.morphia.logging.Logger;
import org.mongodb.morphia.logging.MorphiaLoggerFactory;
import org.mongodb.morphia.mapping.validation.MappingValidator;
import org.mongodb.morphia.utils.ReflectionUtils;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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


/**
 * Represents a mapped class between the MongoDB DBObject and the java POJO.
 * 

* This class will validate classes to make sure they meet the requirement for persistence. * * @author Scott Hernandez */ public class MappedClass { private static final Logger LOG = MorphiaLoggerFactory.get(MappedClass.class); /** * Annotations we are interested in looking for. * * @see #addInterestingAnnotation */ private static final List> INTERESTING_ANNOTATIONS = new ArrayList>(); /** * Annotations interesting for life-cycle events */ @SuppressWarnings("unchecked") private static final List> LIFECYCLE_ANNOTATIONS = asList(PrePersist.class, PreSave.class, PreLoad.class, PostPersist.class, PostLoad.class); static { INTERESTING_ANNOTATIONS.add(Embedded.class); INTERESTING_ANNOTATIONS.add(Entity.class); INTERESTING_ANNOTATIONS.add(EntityListeners.class); INTERESTING_ANNOTATIONS.add(Version.class); INTERESTING_ANNOTATIONS.add(Converters.class); INTERESTING_ANNOTATIONS.add(Indexes.class); INTERESTING_ANNOTATIONS.add(Validation.class); INTERESTING_ANNOTATIONS.add(Field.class); INTERESTING_ANNOTATIONS.add(IndexOptions.class); } /** * Annotations we were interested in, and found. */ private final Map, List> foundAnnotations = new HashMap, List>(); /** * Methods which are life-cycle events */ private final Map, List> lifecycleMethods = new HashMap, List>(); /** * a list of the fields to map */ private final List persistenceFields = new ArrayList(); /** * the type we are mapping to/from */ private final Class clazz; /** * special fields representing the Key of the object */ private java.lang.reflect.Field idField; /** * special annotations representing the type the object */ private Entity entityAn; private Embedded embeddedAn; private MapperOptions mapperOptions; private MappedClass superClass; private List interfaces = new ArrayList(); /** * Creates a MappedClass instance * * @param clazz the class to be mapped * @param mapper the Mapper to use */ public MappedClass(final Class clazz, final Mapper mapper) { this.clazz = clazz; mapperOptions = mapper.getOptions(); if (LOG.isTraceEnabled()) { LOG.trace("Creating MappedClass for " + clazz); } basicValidate(); discover(mapper); if (LOG.isDebugEnabled()) { LOG.debug("MappedClass done: " + toString()); } } /** * This is an internal method subject to change without notice. * * @return the parent class of this type if there is one null otherwise * * @since 1.3 */ public MappedClass getSuperClass() { return superClass; } /** * @return true if the MappedClass is an interface */ public boolean isInterface() { return clazz.isInterface(); } /** * This is an internal method subject to change without notice. * * @return true if the MappedClass is abstract * @since 1.3 */ public boolean isAbstract() { return Modifier.isAbstract(clazz.getModifiers()); } /** * Checks to see if it a Map/Set/List or a property supported by the MongoDB java driver * * @param clazz the type to check * @return true if the type is supported */ public static boolean isSupportedType(final Class clazz) { if (ReflectionUtils.isPropertyType(clazz)) { return true; } if (clazz.isArray() || Map.class.isAssignableFrom(clazz) || Iterable.class.isAssignableFrom(clazz)) { Class subType; if (clazz.isArray()) { subType = clazz.getComponentType(); } else { subType = ReflectionUtils.getParameterizedClass(clazz); } //get component type, String.class from List if (subType != null && subType != Object.class && !ReflectionUtils.isPropertyType(subType)) { return false; } //either no componentType or it is an allowed type return true; } return false; } /** * Adds an annotation for Morphia to retain when mapping. * * @param annotation the type to retain */ public static void addInterestingAnnotation(final Class annotation) { INTERESTING_ANNOTATIONS.add(annotation); } /** * Adds the given Annotation to the internal list for the given Class. * * @param clazz the type to add * @param ann the annotation to add */ public void addAnnotation(final Class clazz, final Annotation ann) { if (ann == null || clazz == null) { return; } if (!foundAnnotations.containsKey(clazz)) { foundAnnotations.put(clazz, new ArrayList()); } foundAnnotations.get(clazz).add(ann); } /** * Call the lifecycle methods * * @param event the lifecycle annotation * @param entity the entity to process * @param dbObj the dbObject to use * @param mapper the Mapper to use * @return dbObj */ @SuppressWarnings({"WMI", "unchecked"}) public DBObject callLifecycleMethods(final Class event, final Object entity, final DBObject dbObj, final Mapper mapper) { final List methodPairs = getLifecycleMethods((Class) event); DBObject retDbObj = dbObj; try { Object tempObj; if (methodPairs != null) { final HashMap, Object> toCall = new HashMap, Object>((int) (methodPairs.size() * 1.3)); for (final ClassMethodPair cm : methodPairs) { toCall.put(cm.clazz, null); } for (final Class c : toCall.keySet()) { if (c != null) { toCall.put(c, getOrCreateInstance(c, mapper)); } } for (final ClassMethodPair cm : methodPairs) { final Method method = cm.method; final Object inst = toCall.get(cm.clazz); method.setAccessible(true); if (LOG.isDebugEnabled()) { LOG.debug(format("Calling lifecycle method(@%s %s) on %s", event.getSimpleName(), method, inst)); } if (inst == null) { if (method.getParameterTypes().length == 0) { tempObj = method.invoke(entity); } else { tempObj = method.invoke(entity, retDbObj); } } else if (method.getParameterTypes().length == 0) { tempObj = method.invoke(inst); } else if (method.getParameterTypes().length == 1) { tempObj = method.invoke(inst, entity); } else { tempObj = method.invoke(inst, entity, retDbObj); } if (tempObj != null) { retDbObj = (DBObject) tempObj; } } } callGlobalInterceptors(event, entity, dbObj, mapper); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } return retDbObj; } /** * Check java field name that will be stored in mongodb * * @param name the name to search for * @return true if a Java field with that name is found */ public boolean containsJavaFieldName(final String name) { return getMappedField(name) != null; } /** * Looks for an annotation of the type given * * @param clazz the type to search for * @return the instance if it was found, if more than one was found, the last one added */ public Annotation getAnnotation(final Class clazz) { final List found = foundAnnotations.get(clazz); return found == null || found.isEmpty() ? null : found.get(found.size() - 1); } /** * Looks for an annotation in the annotations found on a class while mapping * * @param clazz the class to search for * @param the type of annotation to find * @return the instance if it was found, if more than one was found, the last one added */ @SuppressWarnings("unchecked") public List getAnnotations(final Class clazz) { return (List) foundAnnotations.get(clazz); } /** * @return the clazz */ public Class getClazz() { return clazz; } /** * @return the collName */ public String getCollectionName() { if (entityAn == null || entityAn.value().equals(Mapper.IGNORED_FIELDNAME)) { return mapperOptions.isUseLowerCaseCollectionNames() ? clazz.getSimpleName().toLowerCase() : clazz.getSimpleName(); } return entityAn.value(); } /** * @return the embeddedAn */ public Embedded getEmbeddedAnnotation() { return embeddedAn; } /** * @return the entityAn */ public Entity getEntityAnnotation() { return entityAn; } /** * Returns fields annotated with the clazz * * @param clazz The Annotation to find. * @return the list of fields */ public List getFieldsAnnotatedWith(final Class clazz) { final List results = new ArrayList(); for (final MappedField mf : persistenceFields) { if (mf.getAnnotations().containsKey(clazz)) { results.add(mf); } } return results; } /** * Returns the first found Annotation, or null. * * @param clazz The Annotation to find. * @return First found Annotation or null of none found. */ public Annotation getFirstAnnotation(final Class clazz) { final List found = foundAnnotations.get(clazz); return found == null || found.isEmpty() ? null : found.get(0); } /** * @return the idField */ public java.lang.reflect.Field getIdField() { return idField; } /** * Gets the lifecycle methods for particular annotation. * * @param clazz the annotation to search with * @return the list of methods */ public List getLifecycleMethods(final Class clazz) { return lifecycleMethods.get(clazz); } /** * Returns the MappedField by the name that it will stored in mongodb as * * @param storedName the name to search for * @return true if that mapped field name is found */ public MappedField getMappedField(final String storedName) { for (final MappedField mf : persistenceFields) { for (final String n : mf.getLoadNames()) { if (storedName.equals(n)) { return mf; } } } return null; } /** * Returns MappedField for a given java field name on the this MappedClass * * @param name the Java field name to search for * @return the MappedField for the named Java field */ public MappedField getMappedFieldByJavaField(final String name) { for (final MappedField mf : persistenceFields) { if (name.equals(mf.getJavaFieldName())) { return mf; } } return null; } /** * @return the ID field for the class */ public MappedField getMappedIdField() { List fields = getFieldsAnnotatedWith(Id.class); return fields.isEmpty() ? null : fields.get(0); } /** * @return the ID field for the class */ public MappedField getMappedVersionField() { List fields = getFieldsAnnotatedWith(Version.class); return fields.isEmpty() ? null : fields.get(0); } /** * @return the persistenceFields */ public List getPersistenceFields() { return persistenceFields; } /** * @return the relevantAnnotations */ public Map, List> getRelevantAnnotations() { return foundAnnotations; } @Override public int hashCode() { return clazz.hashCode(); } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final MappedClass that = (MappedClass) o; return clazz.equals(that.clazz); } boolean isSubType(final MappedClass mc) { return mc.equals(superClass) || interfaces.contains(mc); } @Override public String toString() { return "MappedClass - kind:" + getCollectionName() + " for " + getClazz().getName() + " fields:" + persistenceFields; } /** * Update mappings based on fields/annotations. */ // TODO: Remove this and make these fields dynamic or auto-set some other way public void update() { embeddedAn = (Embedded) getAnnotation(Embedded.class); entityAn = (Entity) getFirstAnnotation(Entity.class); // polymorphicAn = (Polymorphic) getAnnotation(Polymorphic.class); final List fields = getFieldsAnnotatedWith(Id.class); if (fields != null && !fields.isEmpty()) { idField = fields.get(0).getField(); } } /** * Validates this MappedClass * @param mapper the Mapper to use for validation */ @SuppressWarnings("deprecation") public void validate(final Mapper mapper) { new MappingValidator(mapper.getOptions().getObjectFactory()).validate(mapper, this); } protected void basicValidate() { final boolean isStatic = Modifier.isStatic(clazz.getModifiers()); if (!isStatic && clazz.isMemberClass()) { throw new MappingException("Cannot use non-static inner class: " + clazz + ". Please make static."); } } /** * Discovers interesting (that we care about) things about the class. */ protected void discover(final Mapper mapper) { for (final Class c : INTERESTING_ANNOTATIONS) { addAnnotation(c); } Class superclass = clazz.getSuperclass(); if (superclass != null && !superclass.equals(Object.class)) { superClass = mapper.getMappedClass(superclass); } for (Class aClass : clazz.getInterfaces()) { interfaces.add(mapper.getMappedClass(aClass)); } final List> lifecycleClasses = new ArrayList>(); lifecycleClasses.add(clazz); final EntityListeners entityLisAnn = (EntityListeners) getAnnotation(EntityListeners.class); if (entityLisAnn != null && entityLisAnn.value().length != 0) { Collections.addAll(lifecycleClasses, entityLisAnn.value()); } for (final Class cls : lifecycleClasses) { for (final Method m : ReflectionUtils.getDeclaredAndInheritedMethods(cls)) { for (final Class c : LIFECYCLE_ANNOTATIONS) { if (m.isAnnotationPresent(c)) { addLifecycleEventMethod(c, m, cls.equals(clazz) ? null : cls); } } } } update(); for (final java.lang.reflect.Field field : ReflectionUtils.getDeclaredAndInheritedFields(clazz, true)) { field.setAccessible(true); final int fieldMods = field.getModifiers(); if (!isIgnorable(field, fieldMods, mapper)) { if (field.isAnnotationPresent(Id.class)) { persistenceFields.add(new MappedField(field, clazz, mapper)); update(); } else if (field.isAnnotationPresent(Property.class) || field.isAnnotationPresent(Reference.class) || field.isAnnotationPresent(Embedded.class) || field.isAnnotationPresent(Serialized.class) || isSupportedType(field.getType()) || ReflectionUtils.implementsInterface(field.getType(), Serializable.class)) { persistenceFields.add(new MappedField(field, clazz, mapper)); } else { if (mapper.getOptions().getDefaultMapper() != null) { persistenceFields.add(new MappedField(field, clazz, mapper)); } else if (LOG.isWarningEnabled()) { LOG.warning(format("Ignoring (will not persist) field: %s.%s [type:%s]", clazz.getName(), field.getName(), field.getType().getName())); } } } } } /** * Adds the annotation, if it exists on the field. */ private void addAnnotation(final Class clazz) { final List annotations = ReflectionUtils.getAnnotations(getClazz(), clazz); for (final Annotation ann : annotations) { addAnnotation(clazz, ann); } } private void addLifecycleEventMethod(final Class lceClazz, final Method m, final Class clazz) { final ClassMethodPair cm = new ClassMethodPair(clazz, m); if (lifecycleMethods.containsKey(lceClazz)) { lifecycleMethods.get(lceClazz).add(cm); } else { final List methods = new ArrayList(); methods.add(cm); lifecycleMethods.put(lceClazz, methods); } } private void callGlobalInterceptors(final Class event, final Object entity, final DBObject dbObj, final Mapper mapper) { for (final EntityInterceptor ei : mapper.getInterceptors()) { if (LOG.isDebugEnabled()) { LOG.debug("Calling interceptor method " + event.getSimpleName() + " on " + ei); } if (event.equals(PreLoad.class)) { ei.preLoad(entity, dbObj, mapper); } else if (event.equals(PostLoad.class)) { ei.postLoad(entity, dbObj, mapper); } else if (event.equals(PrePersist.class)) { ei.prePersist(entity, dbObj, mapper); } else if (event.equals(PreSave.class)) { ei.preSave(entity, dbObj, mapper); } else if (event.equals(PostPersist.class)) { ei.postPersist(entity, dbObj, mapper); } } } private Object getOrCreateInstance(final Class clazz, final Mapper mapper) { if (mapper.getInstanceCache().containsKey(clazz)) { return mapper.getInstanceCache().get(clazz); } final Object o = mapper.getOptions().getObjectFactory().createInstance(clazz); final Object nullO = mapper.getInstanceCache().put(clazz, o); if (nullO != null) { if (LOG.isErrorEnabled()) { LOG.error("Race-condition, created duplicate class: " + clazz); } } return o; } private boolean isIgnorable(final java.lang.reflect.Field field, final int fieldMods, final Mapper mapper) { return field.isAnnotationPresent(Transient.class) || Modifier.isTransient(fieldMods) || field.isSynthetic() && Modifier.isTransient(fieldMods) || mapper.getOptions().isIgnoreFinals() && Modifier.isFinal(fieldMods); } private static class ClassMethodPair { private final Class clazz; private final Method method; ClassMethodPair(final Class c, final Method m) { clazz = c; method = m; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy