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

Java tutorial

Introduction

Here is the source code for org.fastmongo.odm.dbobject.mapping.core.DomainToDbObjectConverter.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.google.common.collect.Sets;
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.document.support.LazyProxy;
import org.fastmongo.odm.mapping.support.converter.ConversionService;
import org.fastmongo.odm.mapping.support.linkwriter.LinkWriter;
import org.fastmongo.odm.util.MongoNamingService;
import org.fastmongo.odm.util.ReflectionUtils;

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

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

/**
 * Converts domain models to {@link com.mongodb.DBObject}s.
 * <p/>
 * This implementation is thread safe.
 *
 * @author Alexander Gulko
 */
public class DomainToDbObjectConverter {
    /**
     * Classes that are supported by MongoDB Java Driver as values for document simple fields.
     * We don't need to perform any conversion on the them.
     */
    private static final Set<Class<?>> VALUE_CLASSES = Sets.<Class<?>>newHashSet(Integer.class, int.class,
            Long.class, long.class, Boolean.class, boolean.class, Character.class, char.class, Double.class,
            double.class, Float.class, float.class, String.class, Date.class);

    /**
     * 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;

    /**
     * Returns names of mongo collections and other mapper internal names for the specified classes.
     */
    private MongoNamingService namingService;

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

    /**
     * Classes that will be converted to DBObjects "in place",
     * i.e. without creating a link to actual object (either in external collection or in data section).
     */
    private Set<Class<?>> flatModels = new HashSet<>();

    /**
     * Perform additional actions on links to the bounded class.
     */
    private Map<Class<?>, LinkWriter<?>> linkWriters = new HashMap<>();

    /**
     * The set of Classes stored in separate MongoDB collections.
     */
    private Set<Class<?>> collectionClasses = new HashSet<>();

    /**
     * Constructor with the required dependencies.
     */
    public DomainToDbObjectConverter(ConversionService conversionService, ClassNameResolver classNameResolver,
            MongoNamingService namingService) {
        this.conversionService = conversionService;
        this.classNameResolver = classNameResolver;
        this.namingService = namingService;
    }

    /**
     * Converts the given domain object into the MongoDB document.
     *
     * @param object the object to write into MongoDB.
     * @return the MongoDB document.
     */
    @SuppressWarnings("UnusedDeclaration")
    public <T> BasicDBObject toDbObject(T object) {
        return toTopDbObject(object);
    }

    /**
     * Converts the given root object into the {@link com.mongodb.DBObject}.
     *
     * @param obj the root domain object (e.g. vehicle Make).
     * @return the {@link com.mongodb.DBObject} for the root domain object.
     */
    private BasicDBObject toTopDbObject(Object obj) {
        return toFullDbObject(obj);
    }

    /**
     * Creates a link to the given domain object with id field.
     * <p/>
     * Calls object class {@link org.fastmongo.odm.mapping.support.linkwriter.LinkWriter} if present.
     *
     * @param obj the domain object.
     * @param id  the Id value of the domain object.
     * @return the link to the given domain object with id field.
     */
    @SuppressWarnings("unchecked")
    private DBObject createLink(Object obj, Object id) {
        Class<?> type = obj.getClass();

        // the order matters: 1) link 2) class 3) id and other fields
        BasicDBObject link = new BasicDBObject(LINK_KEY, namingService.getLinkToClass(type));
        writeClass(link, type, true);
        link.append(ID_KEY, id);

        LinkWriter linkWriter = linkWriters.get(type);
        return linkWriter != null ? linkWriter.write(link, obj) : link;
    }

    /**
     * Converts all fields of the given object into {@link com.mongodb.DBObject}.
     *
     * @param obj the domain object.
     * @return the {@link com.mongodb.DBObject} for the given object.
     */
    private BasicDBObject toFullDbObject(final Object obj) {
        final BasicDBObject db = new BasicDBObject();

        Class<?> type = obj.getClass();

        // write class first (it is important!)
        writeClass(db, type, false);

        // then write Id field value
        Field idField = ReflectionUtils.findField(type, ID_KEY);
        if (idField != null) {
            idField.setAccessible(true);
            processField(idField, db, obj);
        }

        // and other fields
        for (Field field : ConverterHelper.getClassFields(type)) {
            if (field != idField) {
                processField(field, db, obj);
            }
        }

        return db;
    }

    /**
     * Converts the value of the given object field to its mongo representation.
     *
     * @param field the field of the domain object.
     * @param db    the partially filled document.
     * @param obj   the domain object.
     */
    private void processField(Field field, BasicDBObject db, Object obj) {
        db.put(field.getName(), toDbValue(getFieldValue(field, obj)));
    }

    /**
     * Converts the given value to its mongo representation.
     *
     * @param value the value to be converted.
     * @return mongo representation of the given value.
     */
    private Object toDbValue(Object value) {
        if (value == null) {
            return null;
        }

        if (value instanceof LazyProxy) {
            // we can be given value from DbObjectToDomainConverter class
            // but proxy doesn't have any field so we need to unwrap it before further processing
            value = ((LazyProxy) value).unwrap();
        }

        Class<?> type = value.getClass();
        Object dbValue;
        if (VALUE_CLASSES.contains(type)) {
            dbValue = value;
        } else if (Enum.class.isAssignableFrom(type)) {
            dbValue = conversionService.convertEnumToString((Enum<?>) value);
        } else if (conversionService.canConvert(type)) {
            dbValue = conversionService.convert(value, type, null);
        } else if (Collection.class.isAssignableFrom(type)) {
            dbValue = toCollection((Collection<?>) value);
        } else if (Map.class.isAssignableFrom(type)) {
            dbValue = toMap((Map<?, ?>) value);
        } else {
            // in this branch we are converting some domain object

            Field idField = ReflectionUtils.findField(type, ID_KEY);
            if (!isCollectionClass(type)) {
                dbValue = toFullDbObject(value);
            } else {
                Object id = getFieldValue(idField, value);
                dbValue = createLink(value, id);
            }
        }

        return dbValue;
    }

    /**
     * Returns <tt>true</tt> if the given class is a class stored in MongoDB Collection.
     *
     * @param clazz the class to check.
     * @return <tt>true</tt> if the given class is a class stored in MongoDB Collection.
     */
    private boolean isCollectionClass(Class<?> clazz) {
        return collectionClasses.contains(clazz);
    }

    /**
     * Converts the given map into {@link com.mongodb.BasicDBObject}.
     *
     * @param map the map to be converted.
     * @return the converted map.
     */
    private BasicDBObject toMap(Map<?, ?> map) {
        BasicDBObject db = new BasicDBObject();

        for (Map.Entry<?, ?> entry : map.entrySet()) {
            String key = entry.getKey().toString();
            db.put(escape(key), toDbValue(entry.getValue()));
        }

        return db;
    }

    /**
     * Converts the given collection into {@link com.mongodb.BasicDBList}.
     *
     * @param col the collection to be converted.
     * @return the converted collection.
     */
    private BasicDBList toCollection(Collection<?> col) {
        final BasicDBList db = new BasicDBList();

        for (Object obj : col) {
            db.add(toDbValue(obj));
        }

        return db;
    }

    /**
     * Writes class name for the document. To restore the appropriate implementation in opposite mapping.
     * <p/>
     * <strong>Should be called before any other fields be added to the document.</strong>
     *
     * @param db     the document.
     * @param type   the class of the document.
     * @param isLink if the document represents a link to actual full document.
     */
    <T extends DBObject> void writeClass(T db, Class<?> type, boolean isLink) {
        if (classNameResolver.isWriteClassName(type)
                || (isLink && classNameResolver.isWriteClassNameToLinks(type))) {
            db.put(CLASS_KEY, cleanClassName(type, classPrefix));
        }
    }

    /*
     * Spring setters.
     */

    @SuppressWarnings("UnusedDeclaration")
    public void setLinkWriters(List<LinkWriter<?>> linkWriterList) {
        for (LinkWriter<?> writer : linkWriterList) {
            for (Class<?> clazz : writer.supportedClasses()) {
                linkWriters.put(clazz, writer);
            }
        }
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setFlatModels(List<Class<?>> classes) {
        flatModels.addAll(classes);
    }

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

    @SuppressWarnings("unused")
    public void setCollectionClasses(Collection<Class<?>> collectionClasses) {
        if (collectionClasses != null) {
            this.collectionClasses.addAll(collectionClasses);
        }
    }
}