org.springframework.data.document.mongodb.convert.MappingMongoConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.document.mongodb.convert.MappingMongoConverter.java

Source

/*
 * Copyright (c) 2011 by the original author(s).
 *
 * 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.springframework.data.document.mongodb.convert;

import static org.springframework.data.document.mongodb.convert.ObjectIdConverters.*;
import static org.springframework.data.mapping.MappingBeanHelper.*;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import com.mongodb.Mongo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.types.ObjectId;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.document.mongodb.mapping.MongoPersistentEntity;
import org.springframework.data.document.mongodb.mapping.MongoPersistentProperty;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.MappingBeanHelper;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.Association;
import org.springframework.data.mapping.model.MappingContext;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.PreferredConstructor;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

/**
 * {@link MongoConverter} that uses a {@link MappingContext} to do sophisticated mapping of domain objects to
 * {@link DBObject}.
 *
 * @author Jon Brisbin <jbrisbin@vmware.com>
 * @author Oliver Gierke
 */
public class MappingMongoConverter extends AbstractMongoConverter
        implements ApplicationContextAware, InitializingBean {

    public static final String CUSTOM_TYPE_KEY = "_class";
    @SuppressWarnings({ "unchecked" })
    private static final List<Class<?>> MONGO_TYPES = Arrays.asList(Number.class, Date.class, String.class,
            DBObject.class);
    private static final List<Class<?>> VALID_ID_TYPES = Arrays
            .asList(new Class<?>[] { ObjectId.class, String.class, BigInteger.class, byte[].class });
    protected static final Log log = LogFactory.getLog(MappingMongoConverter.class);

    protected final GenericConversionService conversionService = ConversionServiceFactory
            .createDefaultConversionService();
    protected final Set<ConvertiblePair> customTypeMapping = new HashSet<ConvertiblePair>();
    protected final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
    protected SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
    protected ApplicationContext applicationContext;
    protected boolean useFieldAccessOnly = true;
    protected Mongo mongo;
    protected String defaultDatabase;

    /**
     * Creates a new {@link MappingMongoConverter} with the given {@link MappingContext}.
     *
     * @param mappingContext
     */
    public MappingMongoConverter(
            MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
        this.mappingContext = mappingContext;
        this.conversionService.removeConvertible(Object.class, String.class);
    }

    /**
     * Add custom {@link Converter} or {@link ConverterFactory} instances to be used that will take presidence over
     * metadata driven conversion between of objects to/from DBObject
     *
     * @param converters
     */
    public void setConverters(List<Converter<?, ?>> converters) {
        if (null != converters) {
            for (Converter<?, ?> c : converters) {
                registerConverter(c);
            }
        }
    }

    /**
     * Inspects the given {@link Converter} for the types it can convert and registers the pair for custom type conversion
     * in case the target type is a Mongo basic type.
     *
     * @param converter
     */
    private void registerConverter(Converter<?, ?> converter) {
        Class<?>[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class);
        if (MONGO_TYPES.contains(arguments[1]) || MONGO_TYPES.contains(arguments[0])) {
            customTypeMapping.add(new ConvertiblePair(arguments[0], arguments[1]));
        }
        conversionService.addConverter(converter);
    }

    private Class<?> getCustomTarget(Class<?> source, Class<?> expectedTargetType) {
        for (ConvertiblePair typePair : customTypeMapping) {
            if (typePair.getSourceType().isAssignableFrom(source)) {

                Class<?> targetType = typePair.getTargetType();

                if (targetType.equals(expectedTargetType) || expectedTargetType == null) {
                    return targetType;
                }
            }
        }

        return null;
    }

    public MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> getMappingContext() {
        return mappingContext;
    }

    public void setMongo(Mongo mongo) {
        this.mongo = mongo;
    }

    public void setDefaultDatabase(String defaultDatabase) {
        this.defaultDatabase = defaultDatabase;
    }

    public boolean isUseFieldAccessOnly() {
        return useFieldAccessOnly;
    }

    public void setUseFieldAccessOnly(boolean useFieldAccessOnly) {
        this.useFieldAccessOnly = useFieldAccessOnly;
    }

    public <T> T convertObjectId(ObjectId id, Class<T> targetType) {
        return conversionService.convert(id, targetType);
    }

    public ObjectId convertObjectId(Object id) {
        return conversionService.convert(id, ObjectId.class);
    }

    public <S extends Object> S read(Class<S> clazz, final DBObject dbo) {
        return read(ClassTypeInformation.from(clazz), dbo);
    }

    @SuppressWarnings("unchecked")
    protected <S extends Object> S read(TypeInformation<S> type, DBObject dbo) {

        if (null == dbo) {
            return null;
        }

        TypeInformation<? extends S> typeToUse = getMoreConcreteTargetType(dbo, type);
        Class<? extends S> rawType = typeToUse.getType();
        Class<?> customTarget = getCustomTarget(rawType, DBObject.class);

        if (customTarget != null) {
            return conversionService.convert(dbo, rawType);
        }

        if (typeToUse.isCollectionLike() && dbo instanceof BasicDBList) {
            List<Object> l = new ArrayList<Object>();
            BasicDBList dbList = (BasicDBList) dbo;
            for (Object o : dbList) {
                if (o instanceof DBObject) {

                    Object newObj = read(typeToUse.getComponentType(), (DBObject) o);
                    Class<?> rawComponentType = typeToUse.getComponentType().getType();

                    if (newObj.getClass().isAssignableFrom(rawComponentType)) {
                        l.add(newObj);
                    } else {
                        l.add(conversionService.convert(newObj, rawComponentType));
                    }
                } else {
                    l.add(o);
                }
            }
            return conversionService.convert(l, rawType);
        }

        // Retrieve persistent entity info
        MongoPersistentEntity<S> persistentEntity = (MongoPersistentEntity<S>) mappingContext
                .getPersistentEntity(typeToUse);
        if (persistentEntity == null) {
            throw new MappingException("No mapping metadata found for " + rawType.getName());
        }

        return read(persistentEntity, dbo);
    }

    private <S extends Object> S read(final MongoPersistentEntity<S> entity, final DBObject dbo) {

        final StandardEvaluationContext spelCtx = new StandardEvaluationContext();
        if (null != applicationContext) {
            spelCtx.setBeanResolver(new BeanFactoryResolver(applicationContext));
        }
        if (!(dbo instanceof BasicDBList)) {
            String[] keySet = dbo.keySet().toArray(new String[] {});
            for (String key : keySet) {
                spelCtx.setVariable(key, dbo.get(key));
            }
        }

        final List<String> ctorParamNames = new ArrayList<String>();
        final MongoPersistentProperty idProperty = entity.getIdProperty();
        final S instance = constructInstance(entity, new PreferredConstructor.ParameterValueProvider() {
            @SuppressWarnings("unchecked")
            public <T> T getParameterValue(PreferredConstructor.Parameter<T> parameter) {
                String name = parameter.getName();
                TypeInformation<T> type = parameter.getType();
                Class<T> rawType = parameter.getRawType();
                String key = idProperty == null ? name
                        : idProperty.getName().equals(name) ? idProperty.getKey() : name;
                Object obj = dbo.get(key);

                ctorParamNames.add(name);
                if (obj instanceof DBRef) {
                    return read(type, ((DBRef) obj).fetch());
                } else if (obj instanceof BasicDBList) {
                    BasicDBList objAsDbList = (BasicDBList) obj;
                    List<?> l = unwrapList(objAsDbList, type);
                    return conversionService.convert(l, rawType);
                } else if (obj instanceof DBObject) {
                    return read(type, ((DBObject) obj));
                } else if (null != obj && obj.getClass().isAssignableFrom(rawType)) {
                    return (T) obj;
                } else if (null != obj) {
                    return conversionService.convert(obj, rawType);
                }

                return null;
            }
        }, spelCtx);

        // Set properties not already set in the constructor
        entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
            public void doWithPersistentProperty(MongoPersistentProperty prop) {

                boolean isConstructorProperty = ctorParamNames.contains(prop.getName());
                boolean hasValueForProperty = dbo.containsField(prop.getKey());

                if (!hasValueForProperty || isConstructorProperty) {
                    return;
                }

                Object obj = getValueInternal(prop, dbo, spelCtx, prop.getSpelExpression());
                try {
                    setProperty(instance, prop, obj, useFieldAccessOnly);
                } catch (IllegalAccessException e) {
                    throw new MappingException(e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    throw new MappingException(e.getMessage(), e);
                }
            }
        });

        // Handle associations
        entity.doWithAssociations(new AssociationHandler<MongoPersistentProperty>() {
            public void doWithAssociation(Association<MongoPersistentProperty> association) {
                MongoPersistentProperty inverseProp = association.getInverse();
                Object obj = getValueInternal(inverseProp, dbo, spelCtx, inverseProp.getSpelExpression());
                try {
                    setProperty(instance, inverseProp, obj);
                } catch (IllegalAccessException e) {
                    throw new MappingException(e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    throw new MappingException(e.getMessage(), e);
                }
            }
        });

        return instance;
    }

    /**
     * Root entry method into write conversion. Adds a type discriminator to the {@link DBObject}. Shouldn't be called for
     * nested conversions.
     * 
     * @see org.springframework.data.document.mongodb.MongoWriter#write(java.lang.Object, com.mongodb.DBObject)
     */
    public void write(final Object obj, final DBObject dbo) {

        if (null == obj) {
            return;
        }

        boolean handledByCustomConverter = getCustomTarget(obj.getClass(), DBObject.class) != null;

        if (!handledByCustomConverter) {
            dbo.put(CUSTOM_TYPE_KEY, obj.getClass().getName());
        }

        writeInternal(obj, dbo);
    }

    /**
     * Internal write conversion method which should be used for nested invocations.
     * 
     * @param obj
     * @param dbo
     */
    @SuppressWarnings("unchecked")
    protected void writeInternal(final Object obj, final DBObject dbo) {

        if (null == obj) {
            return;
        }

        Class<?> customTarget = getCustomTarget(obj.getClass(), DBObject.class);

        if (customTarget != null) {
            DBObject result = conversionService.convert(obj, DBObject.class);
            dbo.putAll(result);
            return;
        }

        if (Map.class.isAssignableFrom(obj.getClass())) {
            writeMapInternal((Map<Object, Object>) obj, dbo);
            return;
        }

        MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(obj.getClass());
        writeInternal(obj, dbo, entity);
    }

    protected void writeInternal(final Object obj, final DBObject dbo, MongoPersistentEntity<?> entity) {

        if (obj == null) {
            return;
        }

        if (null == entity) {
            throw new MappingException("No mapping metadata found for entity of type " + obj.getClass().getName());
        }

        // Write the ID
        final MongoPersistentProperty idProperty = entity.getIdProperty();
        if (!dbo.containsField("_id") && null != idProperty) {
            Object idObj;
            try {
                idObj = getProperty(obj, idProperty, Object.class, useFieldAccessOnly);
            } catch (IllegalAccessException e) {
                throw new MappingException(e.getMessage(), e);
            } catch (InvocationTargetException e) {
                throw new MappingException(e.getMessage(), e);
            }

            if (null != idObj) {
                dbo.put("_id", idObj);
            } else {
                if (!VALID_ID_TYPES.contains(idProperty.getType())) {
                    throw new MappingException("Invalid data type " + idProperty.getType().getName()
                            + " for Id property. Should be one of " + VALID_ID_TYPES);
                }
            }
        }

        // Write the properties
        entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
            public void doWithPersistentProperty(MongoPersistentProperty prop) {
                String name = prop.getName();
                Class<?> type = prop.getType();
                Object propertyObj;
                try {
                    propertyObj = getProperty(obj, prop, type, useFieldAccessOnly);
                } catch (IllegalAccessException e) {
                    throw new MappingException(e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    throw new MappingException(e.getMessage(), e);
                }
                if (null != propertyObj) {
                    if (!isSimpleType(propertyObj.getClass())) {
                        writePropertyInternal(prop, propertyObj, dbo);
                    } else {
                        dbo.put(name, propertyObj);
                    }
                }
            }
        });

        entity.doWithAssociations(new AssociationHandler<MongoPersistentProperty>() {
            public void doWithAssociation(Association<MongoPersistentProperty> association) {
                MongoPersistentProperty inverseProp = association.getInverse();
                Class<?> type = inverseProp.getType();
                Object propertyObj;
                try {
                    propertyObj = getProperty(obj, inverseProp, type, useFieldAccessOnly);
                } catch (IllegalAccessException e) {
                    throw new MappingException(e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    throw new MappingException(e.getMessage(), e);
                }
                if (null != propertyObj) {
                    writePropertyInternal(inverseProp, propertyObj, dbo);
                }
            }
        });
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * Registers converters for {@link ObjectId} handling, removes plain {@link #toString()} converter and promotes the
     * configured {@link ConversionService} to {@link MappingBeanHelper}.
     */
    private void initializeConverters() {

        if (!conversionService.canConvert(ObjectId.class, String.class)) {
            conversionService.addConverter(ObjectIdToStringConverter.INSTANCE);
        }
        if (!conversionService.canConvert(String.class, ObjectId.class)) {
            conversionService.addConverter(StringToObjectIdConverter.INSTANCE);
        }
        if (!conversionService.canConvert(ObjectId.class, BigInteger.class)) {
            conversionService.addConverter(ObjectIdToBigIntegerConverter.INSTANCE);
        }
        if (!conversionService.canConvert(BigInteger.class, ObjectId.class)) {
            conversionService.addConverter(BigIntegerToObjectIdConverter.INSTANCE);
        }

        setConversionService(conversionService);
    }

    @SuppressWarnings({ "unchecked" })
    protected void writePropertyInternal(MongoPersistentProperty prop, Object obj, DBObject dbo) {
        org.springframework.data.document.mongodb.mapping.DBRef dbref = prop.getField()
                .getAnnotation(org.springframework.data.document.mongodb.mapping.DBRef.class);

        String name = prop.getName();
        Class<?> type = prop.getType();
        if (prop.isCollection()) {
            BasicDBList dbList = new BasicDBList();
            Collection<?> coll;
            if (type.isArray()) {
                coll = new ArrayList<Object>();
                for (Object o : (Object[]) obj) {
                    ((List<Object>) coll).add(o);
                }
            } else {
                coll = (Collection<?>) obj;
            }
            for (Object propObjItem : coll) {
                if (null != dbref) {
                    DBRef dbRef = createDBRef(propObjItem, dbref);
                    dbList.add(dbRef);
                } else if (type.isArray() && isSimpleType(prop.getComponentType())) {
                    dbList.add(propObjItem);
                } else if (propObjItem instanceof List) {
                    List<?> propObjColl = (List<?>) propObjItem;
                    TypeInformation<?> typeInfo = ClassTypeInformation.from(propObjItem.getClass());
                    while (typeInfo.isCollectionLike()) {
                        typeInfo = typeInfo.getComponentType();
                    }
                    if (isSimpleType(typeInfo.getType())) {
                        dbList.add(propObjColl);
                    } else {
                        BasicDBList propNestedDbList = new BasicDBList();
                        for (Object propNestedObjItem : propObjColl) {
                            BasicDBObject propDbObj = new BasicDBObject();
                            writeInternal(propNestedObjItem, propDbObj);
                            propNestedDbList.add(propDbObj);
                        }
                        dbList.add(propNestedDbList);
                    }
                } else if (isSimpleType(propObjItem.getClass())) {
                    dbList.add(propObjItem);
                } else {
                    BasicDBObject propDbObj = new BasicDBObject();
                    writeInternal(propObjItem, propDbObj,
                            mappingContext.getPersistentEntity(prop.getComponentType()));
                    dbList.add(propDbObj);
                }
            }
            dbo.put(name, dbList);
            return;
        }

        if (null != obj && obj instanceof Map) {
            BasicDBObject mapDbObj = new BasicDBObject();
            writeMapInternal((Map<Object, Object>) obj, mapDbObj);
            dbo.put(name, mapDbObj);
            return;
        }

        if (null != dbref) {
            DBRef dbRefObj = createDBRef(obj, dbref);
            if (null != dbRefObj) {
                dbo.put(name, dbRefObj);
                return;
            }
        }

        // Lookup potential custom target type
        Class<?> basicTargetType = getCustomTarget(obj.getClass(), null);

        if (basicTargetType != null) {
            dbo.put(name, conversionService.convert(obj, basicTargetType));
            return;
        }

        BasicDBObject propDbObj = new BasicDBObject();
        writeInternal(obj, propDbObj, mappingContext.getPersistentEntity(prop.getTypeInformation()));
        dbo.put(name, propDbObj);
    }

    protected void writeMapInternal(Map<Object, Object> obj, DBObject dbo) {
        for (Map.Entry<Object, Object> entry : obj.entrySet()) {
            Object key = entry.getKey();
            Object val = entry.getValue();
            if (isSimpleType(key.getClass())) {
                // Don't use conversion service here as removal of ObjectToString converter results in some primitive types not
                // being convertable
                String simpleKey = key.toString();
                if (isSimpleType(val.getClass())) {
                    dbo.put(simpleKey, val);
                } else {
                    DBObject newDbo = new BasicDBObject();
                    Class<?> componentType = val.getClass();
                    if (componentType.isArray() || componentType.isAssignableFrom(Collection.class)
                            || componentType.isAssignableFrom(List.class)) {
                        Class<?> ctype = val.getClass().getComponentType();
                        dbo.put("_class", (null != ctype ? ctype.getName() : componentType.getName()));
                    } else {
                        dbo.put("_class", componentType.getName());
                    }
                    writeInternal(val, newDbo);
                    dbo.put(simpleKey, newDbo);
                }
            } else {
                throw new MappingException("Cannot use a complex object as a key value.");
            }
        }
    }

    protected DBRef createDBRef(Object target, org.springframework.data.document.mongodb.mapping.DBRef dbref) {
        MongoPersistentEntity<?> targetEntity = mappingContext.getPersistentEntity(target.getClass());
        if (null == targetEntity || null == targetEntity.getIdProperty()) {
            return null;
        }

        MongoPersistentProperty idProperty = targetEntity.getIdProperty();
        ObjectId id = null;
        try {
            id = getProperty(target, idProperty, ObjectId.class, useFieldAccessOnly);
            if (null == id) {
                throw new MappingException("Cannot create a reference to an object with a NULL id.");
            }
        } catch (IllegalAccessException e) {
            throw new MappingException(e.getMessage(), e);
        } catch (InvocationTargetException e) {
            throw new MappingException(e.getMessage(), e);
        }

        String collection = dbref.collection();
        if ("".equals(collection)) {
            collection = targetEntity.getType().getSimpleName().toLowerCase();
        }

        String dbname = dbref.db();
        if ("".equals(dbname)) {
            dbname = defaultDatabase;
        }

        DB db = mongo.getDB(dbname);
        return new DBRef(db, collection, id);
    }

    @SuppressWarnings({ "unchecked" })
    protected Object getValueInternal(MongoPersistentProperty prop, DBObject dbo, StandardEvaluationContext ctx,
            String spelExpr) {

        Object o;
        if (null != spelExpr) {
            Expression x = spelExpressionParser.parseExpression(spelExpr);
            o = x.getValue(ctx);
        } else {

            Object dbObj = dbo.get(prop.getKey());

            if (dbObj == null) {
                return null;
            }

            Class<?> propertyType = prop.getType();
            Class<?> customTarget = getCustomTarget(dbObj.getClass(), propertyType);

            if (customTarget != null) {
                return conversionService.convert(dbObj, propertyType);
            }

            if (dbObj instanceof DBRef) {
                dbObj = ((DBRef) dbObj).fetch();
            }
            if (dbObj instanceof DBObject) {
                if (prop.isMap() && dbObj instanceof DBObject) {

                    // We have to find a potentially stored class to be used first.
                    Class<?> toType = findTypeToBeUsed((DBObject) dbObj);
                    Map<String, Object> m = new LinkedHashMap<String, Object>();

                    for (Map.Entry<String, Object> entry : ((Map<String, Object>) ((DBObject) dbObj).toMap())
                            .entrySet()) {
                        if (entry.getKey().equals(CUSTOM_TYPE_KEY)) {
                            continue;
                        }
                        if (null != entry.getValue() && entry.getValue() instanceof DBObject) {
                            m.put(entry.getKey(), read((null != toType ? toType : prop.getMapValueType()),
                                    (DBObject) entry.getValue()));
                        } else {
                            m.put(entry.getKey(), entry.getValue());
                        }
                    }
                    return m;
                } else if (prop.isArray() && dbObj instanceof BasicDBObject
                        && ((DBObject) dbObj).keySet().size() == 0) {
                    // It's empty
                    return Array.newInstance(prop.getComponentType(), 0);
                } else if (prop.isCollection() && dbObj instanceof BasicDBList) {
                    BasicDBList dbObjList = (BasicDBList) dbObj;
                    List<Object> items = new LinkedList<Object>();
                    for (int i = 0; i < dbObjList.size(); i++) {
                        Object dbObjItem = dbObjList.get(i);
                        if (dbObjItem instanceof DBRef) {
                            items.add(read(prop.getComponentType(), ((DBRef) dbObjItem).fetch()));
                        } else if (dbObjItem instanceof DBObject) {
                            items.add(read(prop.getComponentType(), (DBObject) dbObjItem));
                        } else {
                            items.add(dbObjItem);
                        }
                    }
                    List<Object> itemsToReturn = new LinkedList<Object>();
                    for (Object obj : items) {
                        itemsToReturn.add(obj);
                    }
                    return itemsToReturn;
                }

                Class<?> toType = findTypeToBeUsed((DBObject) dbObj);

                // It's a complex object, have to read it in
                if (toType != null) {
                    dbo.removeField(CUSTOM_TYPE_KEY);
                    o = read(toType, (DBObject) dbObj);
                } else {
                    o = read(mappingContext.getPersistentEntity(prop.getTypeInformation()), (DBObject) dbObj);
                }
            } else {
                o = dbObj;
            }
        }
        return o;
    }

    /**
     * Returns the type to be used to convert the DBObject given to.
     *
     * @param dbObject
     * @return
     */
    protected Class<?> findTypeToBeUsed(DBObject dbObject) {
        Object classToBeUsed = dbObject.get(CUSTOM_TYPE_KEY);

        if (classToBeUsed == null) {
            return null;
        }

        try {
            return Class.forName(classToBeUsed.toString());
        } catch (ClassNotFoundException e) {
            throw new MappingException(e.getMessage(), e);
        }
    }

    /**
     * Inspects the a custom class definition stored inside the given {@link DBObject} and returns that in case it's a
     * subtype of the given basic one.
     * 
     * @param dbObject
     * @param basicType
     * @return
     */
    @SuppressWarnings("unchecked")
    private <S> TypeInformation<? extends S> getMoreConcreteTargetType(DBObject dbObject,
            TypeInformation<S> basicType) {
        Class<?> documentsTargetType = findTypeToBeUsed(dbObject);
        Class<S> rawType = basicType.getType();
        boolean isMoreConcreteCustomType = documentsTargetType != null
                && rawType.isAssignableFrom(documentsTargetType);
        return isMoreConcreteCustomType
                ? (TypeInformation<? extends S>) ClassTypeInformation.from(documentsTargetType)
                : basicType;
    }

    protected <T> List<?> unwrapList(BasicDBList dbList, TypeInformation<T> targetType) {
        List<Object> rootList = new LinkedList<Object>();
        for (int i = 0; i < dbList.size(); i++) {
            Object obj = dbList.get(i);
            if (obj instanceof BasicDBList) {
                rootList.add(unwrapList((BasicDBList) obj, targetType.getComponentType()));
            } else if (obj instanceof DBObject) {
                rootList.add(read(targetType, (DBObject) obj));
            } else {
                rootList.add(obj);
            }
        }
        return rootList;
    }

    public void afterPropertiesSet() {
        initializeConverters();
    }

}