org.fastmongo.odm.dbobject.mapping.core.DbObjectToDomainConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.fastmongo.odm.dbobject.mapping.core.DbObjectToDomainConverter.java

Source

/*
 * Copyright (c) 2014 Alexander Gulko <kirhog at gmail dot com>.
 *
 * 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.fastmongo.odm.dbobject.mapping.core;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.fastmongo.odm.dbobject.mapping.support.classname.ClassNameResolver;
import org.fastmongo.odm.mapping.core.*;
import org.fastmongo.odm.mapping.support.converter.ConversionService;
import org.fastmongo.odm.mapping.support.linkresolver.LinkResolver;
import org.fastmongo.odm.mapping.support.linkresolver.LinkResolverFactory;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

import static org.fastmongo.odm.dbobject.mapping.core.ConverterHelper.*;

/**
 * Converts {@link com.mongodb.DBObject} to domain objects.
 *
 * @author Alexander Gulko
 */
public class DbObjectToDomainConverter {
    /**
     * Resolves links to internal/external documents.
     */
    private LinkResolverFactory linkResolverFactory;

    /**
     * A service that converts simple values (e.g. BigDecimal to String).
     */
    private ConversionService conversionService;

    /**
     * Used to map between DBObjects and corresponding domain class.
     */
    private ClassNameResolver classNameResolver;

    /**
     * A prefix for full class names to reduce the length of strings in mongo.
     */
    private String classPrefix;

    /**
     * Creates {@link java.util.Set}s with the predefined size.
     */
    private SetFactory setFactory;

    /**
     * Creates {@link java.util.List}s with the predefined size.
     */
    private ListFactory listFactory;

    /**
     * Constructor with the required dependencies.
     */
    public DbObjectToDomainConverter(LinkResolverFactory linkResolverFactory, ConversionService conversionService,
            ClassNameResolver classNameResolver) {
        this.linkResolverFactory = linkResolverFactory;
        this.conversionService = conversionService;
        this.classNameResolver = classNameResolver;

        setFactory = new SetFactory();
        listFactory = new ListFactory();
    }

    /**
     * Converts the given document to object of the specified class.
     *
     * @param root  the root document (nullable).
     * @param clazz the document class.
     * @return the converted object or <tt>null</tt> if root is <tt>null</tt>.
     */
    @SuppressWarnings("unchecked")
    public <T> T fromDbObject(DBObject root, Class<?> clazz) {
        if (root == null) {
            return null;
        }

        String className = classNameResolver.getClassName(clazz, root, classPrefix);
        T obj = newInstance(className);
        Context context = createContext(root, clazz);

        fillObject(obj, root, context);

        return obj;
    }

    /**
     * Creates context for the given root documents and the corresponding collection class.
     *
     * @param root            the root document.
     * @param collectionClass the document class.
     * @return the context for the root document.
     */
    private Context createContext(DBObject root, Class<?> collectionClass) {
        LinkResolver linkResolver = linkResolverFactory.create(root, collectionClass);

        return new Context(linkResolver);
    }

    /**
     * Fills the given object with the {@link com.mongodb.DBObject} using the provided context.
     *
     * @param db      the document which represents the domain object.
     * @param obj     the domain object.
     * @param context the context for this operation, bounded to the root document.
     */
    private void fillObject(final Object obj, final DBObject db, Context context) {
        for (Field field : getClassFields(obj.getClass())) {
            processField(field, obj, db, context);
        }
    }

    /**
     * Converts mongo value for the given object field into the corresponding field value.
     *
     * @param field   the domain object field.
     * @param obj     the domain object.
     * @param db      the document which represents the domain object.
     * @param context the context for this operation, bounded to the root document.
     */
    @SuppressWarnings("unchecked")
    private void processField(Field field, Object obj, DBObject db, Context context) {
        Object value = db.get(field.getName());
        if (value == null) {
            return;
        }

        Class<?> dbValueType = value.getClass();
        Class<?> fieldType = field.getType();
        if (isCollection(fieldType)) {
            if (Set.class.isAssignableFrom(fieldType)) {
                processSet(field, obj, (List<Object>) value, context);
            } else {
                processList(field, obj, (List<Object>) value, context);
            }
        } else if (isMap(fieldType)) {
            Map<String, Object> map = new LinkedHashMap<>();
            setFieldValue(field, obj, map);
            fillMap(map, (DBObject) value, ConverterHelper.getFieldGenericTypes(field)[1], context);
        } else if (!isMap(dbValueType)) {
            setFieldValue(field, obj, toSimpleType(fieldType, value));
        } else {
            DBObject fieldDbValue = (DBObject) value;
            if (fieldDbValue.containsField(LINK_KEY)) {
                processLink(field, obj, fieldDbValue, context);
            } else {
                Object fieldValue = newInstance(
                        classNameResolver.getClassName(field.getGenericType(), fieldDbValue, classPrefix));
                setFieldValue(field, obj, fieldValue);
                fillObject(fieldValue, fieldDbValue, context);
            }
        }
    }

    /**
     * Checks if the class is-a {@link java.util.Map} class.
     *
     * @param type the class to check.
     * @return <tt>true</tt> if the class is-a Map.
     */
    private static boolean isMap(Class<?> type) {
        return Map.class.isAssignableFrom(type);
    }

    /**
     * Checks if the class is-a {@link java.util.Collection} class.
     *
     * @param type the class to check.
     * @return <tt>true</tt> if the class is-a Collection.
     */
    private static boolean isCollection(Class<?> type) {
        return Collection.class.isAssignableFrom(type);
    }

    /**
     * Converts the given mongo value to a value of the target type.
     *
     * @param targetType the target type of value.
     * @param dbValue    the mongo value.
     * @return the value with the target type.
     */
    private Object toSimpleType(Class targetType, Object dbValue) {
        if (Enum.class.isAssignableFrom(targetType)) {
            return conversionService.convertStringToEnum(targetType, (String) dbValue);
        } else if (conversionService.canConvert(targetType)) {
            return conversionService.convert(dbValue, dbValue.getClass(), targetType);
        } else {
            return dbValue;
        }
    }

    /**
     * Converts list of mongo values to the {@link java.util.List} for the given field.
     *
     * @param field   the domain object field with {@link java.util.List} type.
     * @param obj     the domain object.
     * @param dbList  the list of mongo values.
     * @param context the context for this operation, bounded to the root document.
     */
    private void processList(Field field, Object obj, List<Object> dbList, Context context) {
        processCollection(field, obj, dbList, listFactory, context);
    }

    /**
     * Converts list of mongo values to the {@link java.util.Set} for the given field.
     *
     * @param field   the domain object field with {@link java.util.Set} type.
     * @param obj     the domain object.
     * @param dbList  the list of mongo values.
     * @param context the context for this operation, bounded to the root document.
     */
    private void processSet(Field field, Object obj, List<Object> dbList, Context context) {
        processCollection(field, obj, dbList, setFactory, context);
    }

    /**
     * Creates collection proxy to convert list of mongo values
     * to one of {@link java.util.Collection} implementation for the given field later.
     *
     * @param field   the domain object field with {@link java.util.Collection} type.
     * @param obj     the domain object.
     * @param dbList  the list of mongo values.
     * @param factory the factory to create appropriate {@link java.util.Collection} implementation.
     * @param context the context for this operation, bounded to the root document.
     */
    private void processCollection(Field field, Object obj, List<Object> dbList, CollectionFactory factory,
            Context context) {

        final Class<?> elementType = (Class<?>) ConverterHelper.getFieldGenericTypes(field)[0];

        Collection proxy = ConverterHelper.newProxy(factory.getCollectionClass(),
                new DefaultCollectionLoader(dbList, factory, elementType, context), new FieldReplacer(field, obj));
        setFieldValue(field, obj, proxy);
    }

    /**
     * Fills collection of elements with the given type using provided mongo values.
     *
     * @param collection  the collection of elements.
     * @param dbList      the list of mongo values.
     * @param elementType the type of elements in the result collection.
     * @param context     the context for this operation, bounded to the root document.
     */
    private void fillCollection(Collection<Object> collection, List<Object> dbList, Class<?> elementType,
            Context context) {

        // try to bulk load the whole collection instead of loading inside a loop
        if (tryLoadAndFillEntireCollection(collection, dbList, elementType, context)) {
            // if we successfully loaded and converted collection then return
            return;
        }

        // otherwise convert each element in the loop
        for (Object dbObject : dbList) {
            if (dbObject instanceof DBObject) {
                DBObject db = (DBObject) dbObject;
                String className = classNameResolver.getClassName(elementType, db, classPrefix);
                if (className != null) {
                    String link = (String) db.get(LINK_KEY);
                    if (link != null) {
                        final Object id = db.get(ID_KEY);
                        final Class<?> clazz = loadClass(className);
                        collection.add(loadObject(id, clazz, context));
                    } else {
                        Object element = newInstance(className);
                        fillObject(element, db, context);
                        collection.add(element);
                    }
                } else {
                    collection.add(db.toMap());
                }
            } else {
                collection.add(toSimpleType(elementType, dbObject));
            }
        }
    }

    /**
     * Tries to load collection of external documents in one request to mongo.
     *
     * @param collection  the collection of elements.
     * @param dbList      the list of mongo values.
     * @param elementType the type of elements in the result collection.
     * @param context     the context for this operation, bounded to the root document.
     * @return <tt>true</tt> if collection was loaded and converted, otherwise <tt>false</tt>.
     */
    private boolean tryLoadAndFillEntireCollection(Collection<Object> collection, List<Object> dbList,
            Class<?> elementType, Context context) {

        if (!dbList.isEmpty()) {
            if (dbList.get(0) instanceof DBObject) {
                DBObject db = (DBObject) dbList.get(0);
                String link = (String) db.get(LINK_KEY);
                if (link != null) {
                    String elementClassName = classNameResolver.getClassName(elementType, db, classPrefix);
                    Class<?> elementClass = loadClass(elementClassName); // Mongo collection

                    fillExternalDocumentCollection(collection, dbList, elementType, elementClass, context);
                    return true; // load links from external collection and return
                }
            }
        }

        return false;
    }

    /**
     * Loads and fills top-level domain objects (that are root documents in a separate collection)
     * in the given domain objects collection.
     *
     * @param collection            the domain objects collection.
     * @param dbList                the collection of links to the corresponding root documents
     * @param collectionElementType the type of domain objects in the collection (usually an interface).
     * @param elementConcreteClass  the concrete class of domain objects in the collection.
     * @param context               the context for this operation, bounded to the root document.
     */
    @SuppressWarnings("unchecked")
    private void fillExternalDocumentCollection(Collection<Object> collection, List<Object> dbList,
            Class<?> collectionElementType, Class<?> elementConcreteClass, Context context) {

        for (DBObject db : context.linkResolver.resolveExternalLinks(elementConcreteClass, getIds(dbList))) {
            String className = classNameResolver.getClassName(collectionElementType, db, classPrefix);
            final Class<?> clazz = loadClass(className);
            Object model = fromDbObject(db, clazz);

            collection.add(model);
        }
    }

    /**
     * Fills the result <tt>map</tt> with data from <tt>dbMap</tt> (JSON object from MongoDB).
     *
     * @param map          the result map.
     * @param dbMap        the DBObject.
     * @param mapValueType the class of values in the result map, determined from the instance variable
     *                     that holds this map.
     */
    private void fillMap(final Map<String, Object> map, DBObject dbMap, Type mapValueType, Context context) {
        for (String key : dbMap.keySet()) {
            Object dbValue = dbMap.get(key);
            key = unescape(key);

            Object objValue;
            if (dbValue instanceof BasicDBObject) {
                objValue = fillMapObjectValue(map, mapValueType, key, (BasicDBObject) dbValue, context);
            } else if (dbValue instanceof BasicDBList) {
                objValue = fillMapCollectionValue(mapValueType, (BasicDBList) dbValue, context);
            } else if (mapValueType instanceof Class) {
                objValue = toSimpleType((Class<?>) mapValueType, dbValue);
            } else {
                objValue = dbValue;
            }

            map.put(key, objValue);
        }
    }

    /**
     * Fills collection of elements in a map.
     * This method is needed to determine actual classes of the collection and its elements.
     *
     * @param mapValueType the type of values in the map, assignable from {@link java.util.Collection}.
     * @param dbValue      the corresponded mongo values for this collection.
     * @param context      the context for this operation, bounded to the root document.
     * @return filled collection of elements.
     */
    private Object fillMapCollectionValue(Type mapValueType, BasicDBList dbValue, Context context) {
        // class of value in the map, assignable from Collection
        Class<?> collectionClass;
        // class of elements in the Collection
        Class<?> collectionElementClass;

        if (mapValueType instanceof ParameterizedType) {
            // if type is a parameterized collection then get its type parameter
            ParameterizedType parameterizedType = (ParameterizedType) mapValueType;
            collectionClass = (Class<?>) parameterizedType.getRawType();
            collectionElementClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
        } else {
            collectionClass = (Class<?>) mapValueType;
            collectionElementClass = Object.class;
        }

        Collection<Object> collection;
        if (Set.class.isAssignableFrom(collectionClass)) {
            collection = new LinkedHashSet<>();
        } else {
            collection = new ArrayList<>();
        }

        fillCollection(collection, dbValue, collectionElementClass, context);

        return collection;
    }

    /**
     * Fills and returns an object represented by the given dbObject and stored as a map value.
     *
     * @param map          the map holding this object.
     * @param mapValueType the type of values in the map, not assignable from {@link java.util.Collection}.
     * @param key          the corresponded key in the map for this object.
     * @param dbObject     the mongo DBObject which represents result object.
     * @param context      the context for this operation, bounded to the root document.
     * @return the filled map value.
     */
    private Object fillMapObjectValue(Map<String, Object> map, Type mapValueType, String key,
            BasicDBObject dbObject, Context context) {

        String className = classNameResolver.getClassName(mapValueType, dbObject, classPrefix);
        if (className != null) {
            String link = (String) dbObject.get(LINK_KEY);
            if (link != null) {
                final Object id = dbObject.get(ID_KEY);
                final Class<?> clazz = loadClass(className);
                return newProxy(clazz, new LinkObjectLoader(clazz, id, context), new MapValueReplacer(map, key));
            } else {
                Object obj = newInstance(className);
                fillObject(obj, dbObject, context);
                return obj;
            }
        } else {
            Map<String, Object> childMap = new LinkedHashMap<>();
            fillMap(childMap, dbObject, mapValueType, context);
            return childMap;
        }
    }

    /**
     * Processes (i.e. creates proxy and stores this proxy in the field) the given link to an object.
     *
     * @param field   a domain object field references to the object represented by the given link.
     * @param obj     the domain object holding the field.
     * @param value   the link to other object.
     * @param context the context for this operation, bounded to the root document.
     */
    private void processLink(final Field field, final Object obj, DBObject value, Context context) {
        final String className = classNameResolver.getClassName(field.getGenericType(), value, classPrefix);
        final Object id = value.get(ID_KEY);

        final Class<?> clazz = loadClass(className);
        Object fieldValue = newProxy(clazz, new LinkObjectLoader(clazz, id, context),
                new FieldReplacer(field, obj));
        setFieldValue(field, obj, fieldValue);
    }

    /**
     * Loads domain object from MongoDB or from data section for the given class and identifier and converts it.
     *
     * @param id      the domain object`s Id.
     * @param clazz   the <strong>concrete</strong> class of the domain object.
     * @param context the context for this operation, bounded to the root document.
     * @return the loaded and fully converted domain object.
     */
    @SuppressWarnings("unchecked")
    private Object loadObject(Object id, Class<?> clazz, Context context) {
        DBObject linkRoot = context.linkResolver.resolveExternal(id, clazz);
        return fromDbObject(linkRoot, clazz);
    }

    /**
     * The callback to load link to other domain object. Called from a proxy.
     *
     * @see org.fastmongo.odm.mapping.core.proxy.ProxyInvocationHandler
     */
    class LinkObjectLoader implements ObjectLoader {
        private final Class<?> clazz;
        private final Object id;
        private final Context context;

        LinkObjectLoader(Class<?> clazz, Object id, Context context) {
            this.clazz = clazz;
            this.id = id;
            this.context = context;
        }

        @Override
        public Object load() {
            return loadObject(id, clazz, context);
        }

        @Override
        public Object getId() {
            return id;
        }
    }

    /**
     * The callback to load collection of links to other domain objects. Called from a proxy.
     *
     * @see org.fastmongo.odm.mapping.core.proxy.ProxyInvocationHandler
     */
    class DefaultCollectionLoader implements CollectionLoader {
        private final List<Object> dbList;
        private CollectionFactory factory;
        private final Class<?> elementType;
        private Context context;

        DefaultCollectionLoader(List<Object> dbList, CollectionFactory factory, Class<?> elementType,
                Context context) {
            this.dbList = dbList;
            this.factory = factory;
            this.elementType = elementType;
            this.context = context;
        }

        @Override
        public Object load() {
            Collection<Object> collection = factory.create(dbList.size());
            fillCollection(collection, dbList, elementType, context);
            return collection;
        }

        @Override
        public <T> List<T> getIds() {
            return ConverterHelper.getIds(dbList);
        }

        @Override
        public boolean isEmpty() {
            return dbList == null || dbList.isEmpty();
        }

        @Override
        public int size() {
            return dbList != null ? dbList.size() : 0;
        }
    }

    /**
     * Context is responsible for holding objects bound to the root document.
     * <p/>
     * Though for now it can be replaced to just using link resolver instead, it's left for future needs.
     */
    private static class Context {
        private final LinkResolver linkResolver;

        private Context(LinkResolver linkResolver) {
            this.linkResolver = linkResolver;
        }
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setClassPrefix(String classPrefix) {
        this.classPrefix = classPrefix;
    }
}