org.springframework.data.document.mongodb.MongoTemplate.java Source code

Java tutorial

Introduction

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

Source

/*
 * Copyright 2010-2011 the original author or authors.
 *
 * 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;

import static org.springframework.data.document.mongodb.query.Criteria.*;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.mongodb.BasicDBObject;
import com.mongodb.CommandResult;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.WriteConcern;
import com.mongodb.WriteResult;
import com.mongodb.util.JSON;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.types.ObjectId;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.document.mongodb.MongoPropertyDescriptors.MongoPropertyDescriptor;
import org.springframework.data.document.mongodb.convert.MappingMongoConverter;
import org.springframework.data.document.mongodb.convert.MongoConverter;
import org.springframework.data.document.mongodb.convert.SimpleMongoConverter;
import org.springframework.data.document.mongodb.index.IndexDefinition;
import org.springframework.data.document.mongodb.mapping.MongoPersistentEntity;
import org.springframework.data.document.mongodb.mapping.MongoPersistentProperty;
import org.springframework.data.document.mongodb.mapping.event.AfterConvertEvent;
import org.springframework.data.document.mongodb.mapping.event.AfterLoadEvent;
import org.springframework.data.document.mongodb.mapping.event.AfterSaveEvent;
import org.springframework.data.document.mongodb.mapping.event.BeforeConvertEvent;
import org.springframework.data.document.mongodb.mapping.event.BeforeSaveEvent;
import org.springframework.data.document.mongodb.mapping.event.MongoMappingEvent;
import org.springframework.data.document.mongodb.query.Query;
import org.springframework.data.document.mongodb.query.QueryMapper;
import org.springframework.data.document.mongodb.query.Update;
import org.springframework.data.mapping.MappingBeanHelper;
import org.springframework.data.mapping.model.MappingContext;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.jca.cci.core.ConnectionCallback;
import org.springframework.util.Assert;

/**
 * Primary implementation of {@link MongoOperations}.
 *
 * @author Thomas Risberg
 * @author Graeme Rocher
 * @author Mark Pollack
 * @author Oliver Gierke
 */
public class MongoTemplate implements MongoOperations, ApplicationEventPublisherAware {

    private static final Log LOGGER = LogFactory.getLog(MongoTemplate.class);

    private static final String ID = "_id";

    /*
     * WriteConcern to be used for write operations if it has been specified. Otherwise
     * we should not use a WriteConcern defaulting to the one set for the DB or Collection.
     */
    private WriteConcern writeConcern = null;

    /*
     * WriteResultChecking to be used for write operations if it has been specified. Otherwise
     * we should not do any checking.
     */
    private WriteResultChecking writeResultChecking = WriteResultChecking.NONE;

    private final MongoConverter mongoConverter;
    private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
    private final Mongo mongo;
    private final MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
    private final QueryMapper mapper;

    private String databaseName;
    private String username;
    private String password;
    private ApplicationEventPublisher eventPublisher;

    /**
     * Constructor used for a basic template configuration
     *
     * @param mongo
     * @param databaseName
     */
    public MongoTemplate(Mongo mongo, String databaseName) {
        this(mongo, databaseName, null, null, null);
    }

    /**
     * Constructor used for a template configuration with a custom {@link org.springframework.data.document.mongodb.convert.MongoConverter}
     *
     * @param mongo
     * @param databaseName
     * @param mongoConverter
     */
    public MongoTemplate(Mongo mongo, String databaseName, MongoConverter mongoConverter) {
        this(mongo, databaseName, mongoConverter, null, null);
    }

    /**
     * Constructor used for a template configuration with a custom {@link MongoConverter}
     * and with a specific {@link com.mongodb.WriteConcern} to be used for all database write operations
     *
     * @param mongo
     * @param databaseName
     * @param mongoConverter
     * @param writeConcern
     * @param writeResultChecking
     */
    MongoTemplate(Mongo mongo, String databaseName, MongoConverter mongoConverter, WriteConcern writeConcern,
            WriteResultChecking writeResultChecking) {

        Assert.notNull(mongo);
        Assert.notNull(databaseName);

        this.mongo = mongo;
        this.databaseName = databaseName;
        this.writeConcern = writeConcern;
        this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter() : mongoConverter;

        if (this.mongoConverter instanceof MappingMongoConverter) {
            initializeMappingMongoConverter((MappingMongoConverter) this.mongoConverter);
        }

        this.mappingContext = this.mongoConverter.getMappingContext();
        this.mapper = new QueryMapper(this.mongoConverter);

        if (writeResultChecking != null) {
            this.writeResultChecking = writeResultChecking;
        }
    }

    private final MongoConverter getDefaultMongoConverter() {

        SimpleMongoConverter converter = new SimpleMongoConverter();
        converter.afterPropertiesSet();
        return converter;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.eventPublisher = applicationEventPublisher;
    }

    /**
     * Sets the username to use to connect to the Mongo database
     *
     * @param username The username to use
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * Sets the password to use to authenticate with the Mongo database.
     *
     * @param password The password to use
     */
    public void setPassword(String password) {

        this.password = password;
    }

    /**
     * Sets the database name to be used.
     *
     * @param databaseName
     */
    public void setDatabaseName(String databaseName) {
        Assert.notNull(databaseName);
        this.databaseName = databaseName;
    }

    /**
     * Returns the default {@link org.springframework.data.document.mongodb.convert.MongoConverter}.
     *
     * @return
     */
    public MongoConverter getConverter() {
        return this.mongoConverter;
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#getDefaultCollectionName()
     */
    public String getCollectionName(Class<?> clazz) {
        return this.determineCollectionName(clazz);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#executeCommand(java.lang.String)
     */
    public CommandResult executeCommand(String jsonCommand) {
        return executeCommand((DBObject) JSON.parse(jsonCommand));
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#executeCommand(com.mongodb.DBObject)
     */
    public CommandResult executeCommand(final DBObject command) {

        CommandResult result = execute(new DbCallback<CommandResult>() {
            public CommandResult doInDB(DB db) throws MongoException, DataAccessException {
                return db.command(command);
            }
        });

        String error = result.getErrorMessage();
        if (error != null) {
            // TODO: allow configuration of logging level / throw
            //   throw new InvalidDataAccessApiUsageException("Command execution of " +
            //         command.toString() + " failed: " + error);
            LOGGER.warn("Command execution of " + command.toString() + " failed: " + error);
        }
        return result;
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#execute(org.springframework.data.document.mongodb.DBCallback)
     */
    public <T> T execute(DbCallback<T> action) {

        Assert.notNull(action);

        try {
            DB db = this.getDb();
            return action.doInDB(db);
        } catch (RuntimeException e) {
            throw potentiallyConvertRuntimeException(e);
        }
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#execute(org.springframework.data.document.mongodb.CollectionCallback)
     */
    public <T> T execute(Class<?> entityClass, CollectionCallback<T> callback) {
        return execute(determineCollectionName(entityClass), callback);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#execute(org.springframework.data.document.mongodb.CollectionCallback, java.lang.String)
     */
    public <T> T execute(String collectionName, CollectionCallback<T> callback) {

        Assert.notNull(callback);

        try {
            DBCollection collection = getDb().getCollection(collectionName);
            return callback.doInCollection(collection);
        } catch (RuntimeException e) {
            throw potentiallyConvertRuntimeException(e);
        }
    }

    /**
     * Central callback executing method to do queries against the datastore that requires reading a single object from a
     * collection of objects. It will take the following steps <ol> <li>Execute the given {@link ConnectionCallback} for a
     * {@link DBObject}.</li> <li>Apply the given
     * {@link DbObjectCallback} to each of the {@link DBObject}s to obtain the result.</li> <ol>
     *
     * @param <T>
     * @param collectionCallback the callback to retrieve the {@link DBObject} with
     * @param objectCallback       the {@link DbObjectCallback} to transform {@link DBObject}s into the actual domain type
     * @param collectionName       the collection to be queried
     * @return
     */
    private <T> T execute(CollectionCallback<DBObject> collectionCallback, DbObjectCallback<T> objectCallback,
            String collectionName) {

        try {
            T result = objectCallback.doWith(collectionCallback.doInCollection(getCollection(collectionName)));
            return result;
        } catch (RuntimeException e) {
            throw potentiallyConvertRuntimeException(e);
        }
    }

    /**
     * Central callback executing method to do queries against the datastore that requires reading a collection of
     * objects. It will take the following steps <ol> <li>Execute the given {@link ConnectionCallback} for a
     * {@link DBCursor}.</li> <li>Prepare that {@link DBCursor} with the given {@link CursorPreparer} (will be skipped
     * if {@link CursorPreparer} is {@literal null}</li> <li>Iterate over the {@link DBCursor} and applies the given
     * {@link DbObjectCallback} to each of the {@link DBObject}s collecting the actual result {@link List}.</li> <ol>
     *
     * @param <T>
     * @param collectionCallback the callback to retrieve the {@link DBCursor} with
     * @param preparer                the {@link CursorPreparer} to potentially modify the {@link DBCursor} before ireating over it
     * @param objectCallback       the {@link DbObjectCallback} to transform {@link DBObject}s into the actual domain type
     * @param collectionName       the collection to be queried
     * @return
     */
    private <T> List<T> executeEach(CollectionCallback<DBCursor> collectionCallback, CursorPreparer preparer,
            DbObjectCallback<T> objectCallback, String collectionName) {

        try {
            DBCursor cursor = collectionCallback.doInCollection(getCollection(collectionName));

            if (preparer != null) {
                cursor = preparer.prepare(cursor);
            }

            List<T> result = new ArrayList<T>();

            for (DBObject object : cursor) {
                result.add(objectCallback.doWith(object));
            }

            return result;
        } catch (RuntimeException e) {
            throw potentiallyConvertRuntimeException(e);
        }
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#executeInSession(org.springframework.data.document.mongodb.DBCallback)
     */
    public <T> T executeInSession(final DbCallback<T> action) {

        return execute(new DbCallback<T>() {
            public T doInDB(DB db) throws MongoException, DataAccessException {
                try {
                    db.requestStart();
                    return action.doInDB(db);
                } finally {
                    db.requestDone();
                }
            }
        });
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#createCollection(java.lang.String)
     */
    public DBCollection createCollection(final String collectionName) {
        return doCreateCollection(collectionName, new BasicDBObject());
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#createCollection(java.lang.String, org.springframework.data.document.mongodb.CollectionOptions)
     */
    public DBCollection createCollection(final String collectionName, final CollectionOptions collectionOptions) {
        return doCreateCollection(collectionName, convertToDbObject(collectionOptions));
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#getCollection(java.lang.String)
     */
    public DBCollection getCollection(final String collectionName) {
        return execute(new DbCallback<DBCollection>() {
            public DBCollection doInDB(DB db) throws MongoException, DataAccessException {
                return db.getCollection(collectionName);
            }
        });
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#collectionExists(java.lang.String)
     */
    public boolean collectionExists(final String collectionName) {
        return execute(new DbCallback<Boolean>() {
            public Boolean doInDB(DB db) throws MongoException, DataAccessException {
                return db.collectionExists(collectionName);
            }
        });
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#dropCollection(java.lang.String)
     */
    public void dropCollection(String collectionName) {

        execute(collectionName, new CollectionCallback<Void>() {
            public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException {
                collection.drop();
                return null;
            }
        });
    }

    // Indexing methods

    public void ensureIndex(Class<?> entityClass, IndexDefinition indexDefinition) {
        ensureIndex(determineCollectionName(entityClass), indexDefinition);
    }

    public void ensureIndex(String collectionName, final IndexDefinition indexDefinition) {
        execute(collectionName, new CollectionCallback<Object>() {
            public Object doInCollection(DBCollection collection) throws MongoException, DataAccessException {
                DBObject indexOptions = indexDefinition.getIndexOptions();
                if (indexOptions != null) {
                    collection.ensureIndex(indexDefinition.getIndexKeys(), indexOptions);
                } else {
                    collection.ensureIndex(indexDefinition.getIndexKeys());
                }
                return null;
            }
        });
    }

    // Find methods that take a Query to express the query and that return a single object.

    public <T> T findOne(Query query, Class<T> targetClass) {
        return findOne(determineCollectionName(targetClass), query, targetClass);
    }

    public <T> T findOne(String collectionName, Query query, Class<T> targetClass) {
        return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), targetClass);
    }

    // Find methods that take a Query to express the query and that return a List of objects.

    public <T> List<T> find(Query query, Class<T> targetClass) {
        return find(determineCollectionName(targetClass), query, targetClass);
    }

    public <T> List<T> find(String collectionName, final Query query, Class<T> targetClass) {
        CursorPreparer cursorPreparer = null;
        if (query.getSkip() > 0 || query.getLimit() > 0 || query.getSortObject() != null) {
            cursorPreparer = new CursorPreparer() {

                public DBCursor prepare(DBCursor cursor) {
                    DBCursor cursorToUse = cursor;
                    try {
                        if (query.getSkip() > 0) {
                            cursorToUse = cursorToUse.skip(query.getSkip());
                        }
                        if (query.getLimit() > 0) {
                            cursorToUse = cursorToUse.limit(query.getLimit());
                        }
                        if (query.getSortObject() != null) {
                            cursorToUse = cursorToUse.sort(query.getSortObject());
                        }
                    } catch (RuntimeException e) {
                        throw potentiallyConvertRuntimeException(e);
                    }
                    return cursorToUse;
                }
            };
        }
        return doFind(collectionName, query.getQueryObject(), query.getFieldsObject(), targetClass, cursorPreparer);
    }

    public <T> List<T> find(String collectionName, Query query, Class<T> targetClass, CursorPreparer preparer) {
        return doFind(collectionName, query.getQueryObject(), query.getFieldsObject(), targetClass, preparer);
    }

    // Find methods that take a Query to express the query and that return a single object that is
    // also removed from the collection in the database.

    public <T> T findAndRemove(Query query, Class<T> targetClass) {
        return findAndRemove(determineCollectionName(targetClass), query, targetClass);
    }

    public <T> T findAndRemove(String collectionName, Query query, Class<T> targetClass) {
        return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(),
                query.getSortObject(), targetClass);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#insert(java.lang.Object)
     */
    public void insert(Object objectToSave) {
        insert(determineEntityCollectionName(objectToSave), objectToSave);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#insert(java.lang.String, java.lang.Object)
     */
    public void insert(String collectionName, Object objectToSave) {
        doInsert(collectionName, objectToSave, this.mongoConverter);
    }

    protected <T> void doInsert(String collectionName, T objectToSave, MongoWriter<T> writer) {
        BasicDBObject dbDoc = new BasicDBObject();

        maybeEmitEvent(new BeforeConvertEvent<T>(objectToSave));
        writer.write(objectToSave, dbDoc);

        maybeEmitEvent(new BeforeSaveEvent<T>(objectToSave, dbDoc));
        Object id = insertDBObject(collectionName, dbDoc);

        populateIdIfNecessary(objectToSave, id);
        maybeEmitEvent(new AfterSaveEvent<T>(objectToSave, dbDoc));
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#insertList(java.util.List)
     */
    public void insertList(List<? extends Object> listToSave) {
        doInsertList(listToSave, mongoConverter);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#insertList(java.lang.String, java.util.List)
     */
    public void insertList(String collectionName, List<? extends Object> listToSave) {
        doInsertList(collectionName, listToSave, this.mongoConverter);
    }

    protected <T> void doInsertList(List<? extends T> listToSave, MongoWriter<T> writer) {
        Map<String, List<T>> objs = new HashMap<String, List<T>>();

        for (T o : listToSave) {

            MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(o.getClass());
            if (entity == null) {
                throw new InvalidDataAccessApiUsageException(
                        "No Persitent Entity information found for the class " + o.getClass().getName());
            }
            String collection = entity.getCollection();

            List<T> objList = objs.get(collection);
            if (null == objList) {
                objList = new ArrayList<T>();
                objs.put(collection, objList);
            }
            objList.add(o);

        }

        for (Map.Entry<String, List<T>> entry : objs.entrySet()) {
            doInsertList(entry.getKey(), entry.getValue(), this.mongoConverter);
        }
    }

    protected <T> void doInsertList(String collectionName, List<? extends T> listToSave, MongoWriter<T> writer) {

        Assert.notNull(writer);

        List<DBObject> dbObjectList = new ArrayList<DBObject>();
        for (T o : listToSave) {
            BasicDBObject dbDoc = new BasicDBObject();

            maybeEmitEvent(new BeforeConvertEvent<T>(o));
            writer.write(o, dbDoc);

            maybeEmitEvent(new BeforeSaveEvent<T>(o, dbDoc));
            dbObjectList.add(dbDoc);
        }
        List<ObjectId> ids = insertDBObjectList(collectionName, dbObjectList);
        for (int i = 0; i < listToSave.size(); i++) {
            if (i < ids.size()) {
                T obj = listToSave.get(i);
                populateIdIfNecessary(obj, ids.get(i));
                maybeEmitEvent(new AfterSaveEvent<T>(obj, dbObjectList.get(i)));
            }
        }
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#save(java.lang.Object)
     */
    public void save(Object objectToSave) {
        save(determineEntityCollectionName(objectToSave), objectToSave);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#save(java.lang.String, java.lang.Object)
     */
    public void save(String collectionName, Object objectToSave) {
        doSave(collectionName, objectToSave, this.mongoConverter);
    }

    protected <T> void doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
        BasicDBObject dbDoc = new BasicDBObject();

        maybeEmitEvent(new BeforeConvertEvent<T>(objectToSave));
        writer.write(objectToSave, dbDoc);

        maybeEmitEvent(new BeforeSaveEvent<T>(objectToSave, dbDoc));
        Object id = saveDBObject(collectionName, dbDoc);

        populateIdIfNecessary(objectToSave, id);
        maybeEmitEvent(new AfterSaveEvent<T>(objectToSave, dbDoc));
    }

    protected Object insertDBObject(String collectionName, final DBObject dbDoc) {

        // DATADOC-95: This will prevent null objects from being saved.
        //if (dbDoc.keySet().isEmpty()) {
        //return null;
        //}

        //TODO: Need to move this to more central place
        if (dbDoc.containsField("_id")) {
            if (dbDoc.get("_id") instanceof String) {
                ObjectId oid = convertIdValue(this.mongoConverter, dbDoc.get("_id"));
                if (oid != null) {
                    dbDoc.put("_id", oid);
                }
            }
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(
                    "insert DBObject containing fields: " + dbDoc.keySet() + " in collection: " + collectionName);
        }
        return execute(collectionName, new CollectionCallback<Object>() {
            public Object doInCollection(DBCollection collection) throws MongoException, DataAccessException {
                if (writeConcern == null) {
                    collection.insert(dbDoc);
                } else {
                    collection.insert(dbDoc, writeConcern);
                }
                return dbDoc.get(ID);
            }
        });
    }

    protected List<ObjectId> insertDBObjectList(String collectionName, final List<DBObject> dbDocList) {

        if (dbDocList.isEmpty()) {
            return Collections.emptyList();
        }

        //TODO: Need to move this to more central place
        for (DBObject dbDoc : dbDocList) {
            if (dbDoc.containsField("_id")) {
                if (dbDoc.get("_id") instanceof String) {
                    ObjectId oid = convertIdValue(this.mongoConverter, dbDoc.get("_id"));
                    if (oid != null) {
                        dbDoc.put("_id", oid);
                    }
                }
            }
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("insert list of DBObjects containing " + dbDocList.size() + " items");
        }
        execute(collectionName, new CollectionCallback<Void>() {
            public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException {
                if (writeConcern == null) {
                    collection.insert(dbDocList);
                } else {
                    collection.insert(dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]),
                            writeConcern);
                }
                return null;
            }
        });

        List<ObjectId> ids = new ArrayList<ObjectId>();
        for (DBObject dbo : dbDocList) {
            Object id = dbo.get(ID);
            if (id instanceof ObjectId) {
                ids.add((ObjectId) id);
            } else {
                // no id was generated
                ids.add(null);
            }
        }
        return ids;
    }

    protected Object saveDBObject(String collectionName, final DBObject dbDoc) {

        if (dbDoc.keySet().isEmpty()) {
            return null;
        }

        //TODO: Need to move this to more central place
        if (dbDoc.containsField("_id")) {
            if (dbDoc.get("_id") instanceof String) {
                ObjectId oid = convertIdValue(this.mongoConverter, dbDoc.get("_id"));
                if (oid != null) {
                    dbDoc.put("_id", oid);
                }
            }
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("save DBObject containing fields: " + dbDoc.keySet());
        }
        return execute(collectionName, new CollectionCallback<Object>() {
            public Object doInCollection(DBCollection collection) throws MongoException, DataAccessException {
                if (writeConcern == null) {
                    collection.save(dbDoc);
                } else {
                    collection.save(dbDoc, writeConcern);
                }
                return dbDoc.get(ID);
            }
        });
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#updateFirst(com.mongodb.DBObject, com.mongodb.DBObject)
     */
    public WriteResult updateFirst(Class<?> entityClass, Query query, Update update) {
        return doUpdate(determineCollectionName(entityClass), query, update, entityClass, false, false);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#updateFirst(java.lang.String, com.mongodb.DBObject, com.mongodb.DBObject)
     */
    public WriteResult updateFirst(final String collectionName, final Query query, final Update update) {
        return doUpdate(collectionName, query, update, null, false, false);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#updateMulti(com.mongodb.DBObject, com.mongodb.DBObject)
     */
    public WriteResult updateMulti(Class<?> entityClass, Query query, Update update) {
        return doUpdate(determineCollectionName(entityClass), query, update, entityClass, false, true);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#updateMulti(java.lang.String, com.mongodb.DBObject, com.mongodb.DBObject)
     */
    public WriteResult updateMulti(String collectionName, final Query query, final Update update) {
        return doUpdate(collectionName, query, update, null, false, true);
    }

    protected WriteResult doUpdate(final String collectionName, final Query query, final Update update,
            final Class<?> entityClass, final boolean upsert, final boolean multi) {

        return execute(collectionName, new CollectionCallback<WriteResult>() {
            public WriteResult doInCollection(DBCollection collection) throws MongoException, DataAccessException {
                DBObject queryObj = query.getQueryObject();
                DBObject updateObj = update.getUpdateObject();

                String idProperty = "id";
                if (null != entityClass) {
                    idProperty = getPersistentEntity(entityClass).getIdProperty().getName();
                }
                for (String key : queryObj.keySet()) {
                    if (idProperty.equals(key)) {
                        // This is an ID field
                        queryObj.put(ID, mongoConverter.maybeConvertObject(queryObj.get(key)));
                        queryObj.removeField(key);
                    } else {
                        queryObj.put(key, mongoConverter.maybeConvertObject(queryObj.get(key)));
                    }
                }

                for (String key : updateObj.keySet()) {
                    updateObj.put(key, mongoConverter.maybeConvertObject(updateObj.get(key)));
                }

                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("calling update using query: " + queryObj + " and update: " + updateObj
                            + " in collection: " + collectionName);
                }

                WriteResult wr;
                if (writeConcern == null) {
                    if (multi) {
                        wr = collection.updateMulti(queryObj, updateObj);
                    } else {
                        wr = collection.update(queryObj, updateObj);
                    }
                } else {
                    wr = collection.update(queryObj, updateObj, upsert, multi, writeConcern);
                }
                handleAnyWriteResultErrors(wr, queryObj, "update with '" + updateObj + "'");
                return wr;
            }
        });

    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#remove(com.mongodb.DBObject)
     */
    public void remove(Query query) {
        remove(query, null);
    }

    public void remove(Object object) {
        Object idValue = this.getIdValue(object);
        remove(new Query(whereId().is(idValue)), object.getClass());
    }

    public <T> void remove(Query query, Class<T> targetClass) {
        Assert.notNull(query);
        remove(determineCollectionName(targetClass), query, targetClass);
    }

    public <T> void remove(String collectionName, final Query query, Class<T> targetClass) {
        if (query == null) {
            throw new InvalidDataAccessApiUsageException("Query passed in to remove can't be null");
        }
        final DBObject queryObject = query.getQueryObject();
        final MongoPersistentEntity<?> entity = getPersistentEntity(targetClass);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("remove using query: " + queryObject + " in collection: " + collectionName);
        }
        execute(collectionName, new CollectionCallback<Void>() {
            public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException {
                DBObject dboq = mapper.getMappedObject(queryObject, entity);
                WriteResult wr = null;
                if (writeConcern == null) {
                    wr = collection.remove(dboq);
                } else {
                    wr = collection.remove(dboq, writeConcern);
                }
                handleAnyWriteResultErrors(wr, dboq, "remove");
                return null;
            }
        });
    }

    /* (non-Javadoc)
       * @see org.springframework.data.document.mongodb.MongoOperations#remove(java.lang.String, com.mongodb.DBObject)
       */
    public void remove(String collectionName, final Query query) {
        remove(collectionName, query, null);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.MongoOperations#getCollection(java.lang.Class)
     */
    public <T> List<T> getCollection(Class<T> targetClass) {
        return executeEach(new FindCallback(null), null, new ReadDbObjectCallback<T>(mongoConverter, targetClass),
                determineCollectionName(targetClass));
    }

    public <T> List<T> getCollection(String collectionName, Class<T> targetClass) {
        return executeEach(new FindCallback(null), null, new ReadDbObjectCallback<T>(mongoConverter, targetClass),
                collectionName);
    }

    public Set<String> getCollectionNames() {
        return execute(new DbCallback<Set<String>>() {
            public Set<String> doInDB(DB db) throws MongoException, DataAccessException {
                return db.getCollectionNames();
            }
        });
    }

    public DB getDb() {
        return MongoDbUtils.getDB(mongo, databaseName, username, password == null ? null : password.toCharArray());
    }

    protected <T> void maybeEmitEvent(MongoMappingEvent<T> event) {
        if (null != eventPublisher) {
            eventPublisher.publishEvent(event);
        }
    }

    /**
     * Create the specified collection using the provided options
     *
     * @param collectionName
     * @param collectionOptions
     * @return the collection that was created
     */
    protected DBCollection doCreateCollection(final String collectionName, final DBObject collectionOptions) {
        return execute(new DbCallback<DBCollection>() {
            public DBCollection doInDB(DB db) throws MongoException, DataAccessException {
                DBCollection coll = db.createCollection(collectionName, collectionOptions);
                // TODO: Emit a collection created event
                return coll;
            }
        });
    }

    /**
     * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter
     * <p/>
     * The query document is specified as a standard DBObject and so is the fields specification.
     *
     * @param collectionName name of the collection to retrieve the objects from
     * @param query               the query document that specifies the criteria used to find a record
     * @param fields             the document that specifies the fields to be returned
     * @param targetClass      the parameterized type of the returned list.
     * @return the List of converted objects.
     */
    protected <T> T doFindOne(String collectionName, DBObject query, DBObject fields, Class<T> targetClass) {
        MongoReader<? super T> readerToUse = this.mongoConverter;
        MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(targetClass);
        DBObject mappedQuery = mapper.getMappedObject(query, entity);

        return execute(new FindOneCallback(mappedQuery, fields),
                new ReadDbObjectCallback<T>(readerToUse, targetClass), collectionName);
    }

    /**
     * Map the results of an ad-hoc query on the default MongoDB collection to a List of the specified type.
     * <p/>
     * The object is converted from the MongoDB native representation using an instance of
     * {@see MongoConverter}.    Unless configured otherwise, an
     * instance of SimpleMongoConverter will be used.
     * <p/>
     * The query document is specified as a standard DBObject and so is the fields specification.
     * <p/>
     * Can be overridden by subclasses.
     *
     * @param collectionName name of the collection to retrieve the objects from
     * @param query               the query document that specifies the criteria used to find a record
     * @param fields             the document that specifies the fields to be returned
     * @param targetClass      the parameterized type of the returned list.
     * @param preparer          allows for customization of the DBCursor used when iterating over the result set,
     *                       (apply limits, skips and so on).
     * @return the List of converted objects.
     */
    protected <T> List<T> doFind(String collectionName, DBObject query, DBObject fields, Class<T> targetClass,
            CursorPreparer preparer) {
        MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(targetClass);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("find using query: " + query + " fields: " + fields + " for class: " + targetClass
                    + " in collection: " + collectionName);
        }
        return executeEach(new FindCallback(mapper.getMappedObject(query, entity), fields), preparer,
                new ReadDbObjectCallback<T>(mongoConverter, targetClass), collectionName);
    }

    /**
     * Map the results of an ad-hoc query on the default MongoDB collection to a List using the template's converter.
     * <p/>
     * The query document is specified as a standard DBObject and so is the fields specification.
     *
     * @param collectionName name of the collection to retrieve the objects from
     * @param query               the query document that specifies the criteria used to find a record
     * @param fields             the document that specifies the fields to be returned
     * @param targetClass      the parameterized type of the returned list.
     * @param reader             the MongoReader to convert from DBObject to an object.
     * @return the List of converted objects.
     */
    protected <T> List<T> doFind(String collectionName, DBObject query, DBObject fields, Class<T> targetClass) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("find using query: " + query + " fields: " + fields + " for class: " + targetClass
                    + " in collection: " + collectionName);
        }
        MongoReader<? super T> readerToUse = this.mongoConverter;
        MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(targetClass);
        return executeEach(new FindCallback(mapper.getMappedObject(query, entity), fields), null,
                new ReadDbObjectCallback<T>(readerToUse, targetClass), collectionName);
    }

    protected DBObject convertToDbObject(CollectionOptions collectionOptions) {
        DBObject dbo = new BasicDBObject();
        if (collectionOptions != null) {
            if (collectionOptions.getCapped() != null) {
                dbo.put("capped", collectionOptions.getCapped().booleanValue());
            }
            if (collectionOptions.getSize() != null) {
                dbo.put("size", collectionOptions.getSize().intValue());
            }
            if (collectionOptions.getMaxDocuments() != null) {
                dbo.put("max", collectionOptions.getMaxDocuments().intValue());
            }
        }
        return dbo;
    }

    /**
     * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter.
     * The first document that matches the query is returned and also removed from the collection in the database.
     * <p/>
     * The query document is specified as a standard DBObject and so is the fields specification.
     *
     * @param collectionName name of the collection to retrieve the objects from
     * @param query               the query document that specifies the criteria used to find a record
     * @param targetClass      the parameterized type of the returned list.
     * @param reader             the MongoReader to convert from DBObject to an object.
     * @return the List of converted objects.
     */
    protected <T> T doFindAndRemove(String collectionName, DBObject query, DBObject fields, DBObject sort,
            Class<T> targetClass) {
        MongoReader<? super T> readerToUse = this.mongoConverter;
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("findAndRemove using query: " + query + " fields: " + fields + " sort: " + sort
                    + " for class: " + targetClass + " in collection: " + collectionName);
        }
        MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(targetClass);
        return execute(new FindAndRemoveCallback(mapper.getMappedObject(query, entity), fields, sort),
                new ReadDbObjectCallback<T>(readerToUse, targetClass), collectionName);
    }

    protected Object getIdValue(Object object) {

        MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(object.getClass());
        MongoPersistentProperty idProp = entity.getIdProperty();

        if (idProp == null) {
            throw new MappingException("No id property found for object of type " + entity.getType().getName());
        }

        try {
            return MappingBeanHelper.getProperty(object, idProp, Object.class, true);
        } catch (IllegalAccessException e) {
            throw new MappingException(e.getMessage(), e);
        } catch (InvocationTargetException e) {
            throw new MappingException(e.getMessage(), e);
        }
    }

    /**
     * Populates the id property of the saved object, if it's not set already.
     *
     * @param savedObject
     * @param id
     */
    protected void populateIdIfNecessary(Object savedObject, Object id) {

        if (id == null) {
            return;
        }

        MongoPersistentProperty idProp = getIdPropertyFor(savedObject.getClass());

        if (idProp == null) {
            return;
        }

        try {
            MappingBeanHelper.setProperty(savedObject, idProp, id);
            return;
        } catch (IllegalAccessException e) {
            throw new MappingException(e.getMessage(), e);
        } catch (InvocationTargetException e) {
            throw new MappingException(e.getMessage(), e);
        }
    }

    /**
     * Substitutes the id key if it is found in he query. Any 'id' keys will be replaced with '_id' and the value converted
     * to an ObjectId if possible. This conversion should match the way that the id fields are converted during read
     * operations.
     *
     * @param query
     * @param targetClass
     * @param reader
     */
    protected void substituteMappedIdIfNecessary(DBObject query, Class<?> targetClass, MongoReader<?> reader) {
        MongoConverter converter = null;
        if (reader instanceof SimpleMongoConverter) {
            converter = (MongoConverter) reader;
        } else if (reader instanceof MappingMongoConverter) {
            converter = (MappingMongoConverter) reader;
        } else {
            return;
        }
        String idKey = null;
        if (query.containsField("id")) {
            idKey = "id";
        }
        if (query.containsField("_id")) {
            idKey = "_id";
        }
        if (idKey == null) {
            // no ids in this query
            return;
        }
        MongoPropertyDescriptor descriptor;
        try {
            MongoPropertyDescriptor mpd = new MongoPropertyDescriptor(new PropertyDescriptor(idKey, targetClass),
                    targetClass);
            descriptor = mpd;
        } catch (IntrospectionException e) {
            // no property descriptor for this key - try the other
            try {
                String theOtherIdKey = "id".equals(idKey) ? "_id" : "id";
                MongoPropertyDescriptor mpd2 = new MongoPropertyDescriptor(
                        new PropertyDescriptor(theOtherIdKey, targetClass), targetClass);
                descriptor = mpd2;
            } catch (IntrospectionException e2) {
                // no property descriptor for this key either - bail
                return;
            }
        }
        if (descriptor.isIdProperty() && descriptor.isOfIdType()) {
            Object value = query.get(idKey);
            if (value instanceof DBObject) {
                DBObject dbo = (DBObject) value;
                if (dbo.containsField("$in")) {
                    List<Object> ids = new ArrayList<Object>();
                    int count = 0;
                    for (Object o : (Object[]) dbo.get("$in")) {
                        count++;
                        ObjectId newValue = convertIdValue(converter, o);
                        if (newValue != null) {
                            ids.add(newValue);
                        }
                    }
                    if (ids.size() > 0 && ids.size() != count) {
                        throw new InvalidDataAccessApiUsageException("Inconsistent set of id values provided "
                                + Arrays.asList((Object[]) dbo.get("$in")));
                    }
                    if (ids.size() > 0) {
                        dbo.removeField("$in");
                        dbo.put("$in", ids.toArray());
                    }
                }
                query.removeField(idKey);
                query.put(MongoPropertyDescriptor.ID_KEY, value);
            } else {
                ObjectId newValue = convertIdValue(converter, value);
                query.removeField(idKey);
                if (newValue != null) {
                    query.put(MongoPropertyDescriptor.ID_KEY, newValue);
                } else {
                    query.put(MongoPropertyDescriptor.ID_KEY, value);
                }
            }
        }
    }

    private MongoPersistentEntity<?> getPersistentEntity(Class<?> type) {
        return type == null ? null : mappingContext.getPersistentEntity(type);
    }

    private MongoPersistentProperty getIdPropertyFor(Class<?> type) {
        return mappingContext.getPersistentEntity(type).getIdProperty();
    }

    private ObjectId convertIdValue(MongoConverter converter, Object value) {
        ObjectId newValue = null;
        try {
            if (value instanceof String && ObjectId.isValid((String) value)) {
                newValue = converter.convertObjectId(value);
            }
        } catch (ConversionFailedException iae) {
            LOGGER.warn("Unable to convert the String " + value + " to an ObjectId");
        }
        return newValue;
    }

    private <T> String determineEntityCollectionName(T obj) {
        if (null != obj) {
            return determineCollectionName(obj.getClass());
        }

        return null;
    }

    private String determineCollectionName(Class<?> clazz) {

        if (clazz == null) {
            throw new InvalidDataAccessApiUsageException(
                    "No class parameter provided, entity collection can't be determined for " + clazz);
        }

        MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(clazz);
        if (entity == null) {
            throw new InvalidDataAccessApiUsageException(
                    "No Persitent Entity information found for the class " + clazz.getName());
        }
        return entity.getCollection();
    }

    /**
     * Checks and handles any errors.
     * <p/>
     * TODO: current implementation logs errors - will be configurable to log warning, errors or
     * throw exception in later versions
     */
    private void handleAnyWriteResultErrors(WriteResult wr, DBObject query, String operation) {
        if (WriteResultChecking.NONE == this.writeResultChecking) {
            return;
        }
        String error = wr.getError();
        int n = wr.getN();
        if (error != null) {
            String message = "Execution of '" + operation
                    + (query == null ? "" : "' using '" + query.toString() + "' query") + " failed: " + error;
            if (WriteResultChecking.EXCEPTION == this.writeResultChecking) {
                throw new DataIntegrityViolationException(message);
            } else {
                LOGGER.error(message);
            }
        } else if (n == 0) {
            String message = "Execution of '" + operation
                    + (query == null ? "" : "' using '" + query.toString() + "' query")
                    + " did not succeed: 0 documents updated";
            if (WriteResultChecking.EXCEPTION == this.writeResultChecking) {
                throw new DataIntegrityViolationException(message);
            } else {
                LOGGER.warn(message);
            }
        }

    }

    /**
     * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original
     * exception if the conversation failed. Thus allows safe rethrowing of the return value.
     *
     * @param ex
     * @return
     */
    private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) {
        RuntimeException resolved = this.exceptionTranslator.translateExceptionIfPossible(ex);
        return resolved == null ? ex : resolved;
    }

    private void initializeMappingMongoConverter(MappingMongoConverter converter) {
        converter.setMongo(mongo);
        converter.setDefaultDatabase(databaseName);
    }

    /**
     * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification
     * {@link DBObject} and executes that against the {@link DBCollection}.
     *
     * @author Oliver Gierke
     * @author Thomas Risberg
     */
    private static class FindOneCallback implements CollectionCallback<DBObject> {

        private final DBObject query;

        private final DBObject fields;

        public FindOneCallback(DBObject query, DBObject fields) {
            this.query = query;
            this.fields = fields;
        }

        public DBObject doInCollection(DBCollection collection) throws MongoException, DataAccessException {
            if (fields == null) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(
                            "findOne using query: " + query + " in db.collection: " + collection.getFullName());
                }
                return collection.findOne(query);
            } else {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("findOne using query: " + query + " fields: " + fields + " in db.collection: "
                            + collection.getFullName());
                }
                return collection.findOne(query, fields);
            }
        }
    }

    /**
     * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification
     * {@link DBObject} and executes that against the {@link DBCollection}.
     *
     * @author Oliver Gierke
     * @author Thomas Risberg
     */
    private static class FindCallback implements CollectionCallback<DBCursor> {

        private final DBObject query;

        private final DBObject fields;

        public FindCallback(DBObject query) {
            this(query, null);
        }

        public FindCallback(DBObject query, DBObject fields) {
            this.query = query;
            this.fields = fields;
        }

        public DBCursor doInCollection(DBCollection collection) throws MongoException, DataAccessException {
            if (fields == null) {
                return collection.find(query);
            } else {
                return collection.find(query, fields);
            }
        }
    }

    /**
     * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification
     * {@link DBObject} and executes that against the {@link DBCollection}.
     *
     * @author Thomas Risberg
     */
    private static class FindAndRemoveCallback implements CollectionCallback<DBObject> {

        private final DBObject query;

        private final DBObject fields;

        private final DBObject sort;

        public FindAndRemoveCallback(DBObject query, DBObject fields, DBObject sort) {
            this.query = query;
            this.fields = fields;
            this.sort = sort;
        }

        public DBObject doInCollection(DBCollection collection) throws MongoException, DataAccessException {
            return collection.findAndModify(query, fields, sort, true, null, false, false);
        }
    }

    /**
     * Simple internal callback to allow operations on a {@link DBObject}.
     *
     * @author Oliver Gierke
     */

    private interface DbObjectCallback<T> {

        T doWith(DBObject object);
    }

    /**
     * Simple {@link DbObjectCallback} that will transform {@link DBObject} into the given target type using the given
     * {@link MongoReader}.
     *
     * @author Oliver Gierke
     */
    private class ReadDbObjectCallback<T> implements DbObjectCallback<T> {

        private final MongoReader<? super T> reader;
        private final Class<T> type;

        public ReadDbObjectCallback(MongoReader<? super T> reader, Class<T> type) {
            this.reader = reader;
            this.type = type;
        }

        public T doWith(DBObject object) {
            if (null != object) {
                maybeEmitEvent(new AfterLoadEvent<DBObject>(object));
            }
            T source = reader.read(type, object);
            if (null != source) {
                maybeEmitEvent(new AfterConvertEvent<T>(object, source));
            }
            return source;
        }
    }

    public void setWriteResultChecking(WriteResultChecking resultChecking) {
        this.writeResultChecking = resultChecking;
    }

    public void setWriteConcern(WriteConcern writeConcern) {
        this.writeConcern = writeConcern;
    }

}