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

com.google.code.morphia.DatastoreImpl Maven / Gradle / Ivy

package com.google.code.morphia;


import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.google.code.morphia.annotations.CappedAt;
import com.google.code.morphia.annotations.Entity;
import com.google.code.morphia.annotations.Index;
import com.google.code.morphia.annotations.Indexed;
import com.google.code.morphia.annotations.Indexes;
import com.google.code.morphia.annotations.NotSaved;
import com.google.code.morphia.annotations.PostPersist;
import com.google.code.morphia.annotations.Reference;
import com.google.code.morphia.annotations.Serialized;
import com.google.code.morphia.annotations.Version;
import com.google.code.morphia.logging.Logr;
import com.google.code.morphia.logging.MorphiaLoggerFactory;
import com.google.code.morphia.mapping.MappedClass;
import com.google.code.morphia.mapping.MappedField;
import com.google.code.morphia.mapping.Mapper;
import com.google.code.morphia.mapping.MappingException;
import com.google.code.morphia.mapping.cache.EntityCache;
import com.google.code.morphia.mapping.lazy.DatastoreHolder;
import com.google.code.morphia.mapping.lazy.proxy.ProxyHelper;
import com.google.code.morphia.query.Query;
import com.google.code.morphia.query.QueryException;
import com.google.code.morphia.query.QueryImpl;
import com.google.code.morphia.query.UpdateException;
import com.google.code.morphia.query.UpdateOperations;
import com.google.code.morphia.query.UpdateOpsImpl;
import com.google.code.morphia.query.UpdateResults;
import com.google.code.morphia.utils.Assert;
import com.google.code.morphia.utils.IndexDirection;
import com.google.code.morphia.utils.IndexFieldDef;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.CommandResult;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBDecoderFactory;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import com.mongodb.MapReduceCommand;
import com.mongodb.MapReduceCommand.OutputType;
import com.mongodb.MapReduceOutput;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.WriteConcern;
import com.mongodb.WriteResult;


/**
 * A generic (type-safe) wrapper around mongodb collections
 *
 * @author Scott Hernandez
 */
@SuppressWarnings({"unchecked", "deprecation"})
public class DatastoreImpl implements AdvancedDatastore {
  private static final Logr LOG = MorphiaLoggerFactory.get(DatastoreImpl.class);

  protected final Mapper mapper;
  protected final Mongo  mongo;
  protected final DB     db;
  protected WriteConcern     defConcern     = WriteConcern.SAFE;
  protected DBDecoderFactory decoderFactory;

  public DatastoreImpl(final Mapper mapper, final Mongo mongo, final String dbName) {
    this.mapper = mapper;
    this.mongo = mongo;
    db = mongo.getDB(dbName);

    // VERY discussable
    DatastoreHolder.getInstance().set(this);
  }

  public DatastoreImpl(final Morphia morphia, final Mongo mongo) {
    this(morphia, mongo, null);
  }

  public DatastoreImpl(final Morphia morphia, final Mongo mongo, final String dbName, final String username, final char[] password) {
    this(morphia.getMapper(), mongo, dbName);

    if (username != null) {
      if (!db.authenticate(username, password)) {
        throw new AuthenticationException(
          "User '" + username + "' cannot be authenticated with the given password for database '" + dbName + "'");
      }
    }

  }

  public DatastoreImpl(final Morphia morphia, final Mongo mongo, final String dbName) {
    this(morphia.getMapper(), mongo, dbName);
  }

  public DatastoreImpl copy(final String database) {
    return new DatastoreImpl(mapper, mongo, database);
  }

  public  DBRef createRef(final Class clazz, final V id) {
    if (id == null) {
      throw new MappingException("Could not get id for " + clazz.getName());
    }
    return new DBRef(getDB(), getCollection(clazz).getName(), id);
  }


  public  DBRef createRef(final T entity) {
    final T wrapped = ProxyHelper.unwrap(entity);
    final Object id = getId(wrapped);
    if (id == null) {
      throw new MappingException("Could not get id for " + wrapped.getClass().getName());
    }
    return createRef(wrapped.getClass(), id);
  }

  @Deprecated
  protected Object getId(final Object entity) {
    return mapper.getId(entity);
  }

  @Deprecated // use mapper instead.
  public  Key getKey(final T entity) {
    return mapper.getKey(entity);
  }

  public  WriteResult delete(final String kind, final T id) {
    final DBCollection dbColl = getCollection(kind);
    final WriteResult wr = dbColl.remove(BasicDBObjectBuilder.start().add(Mapper.ID_KEY, id).get());
    throwOnError(null, wr);
    return wr;
  }

  public  WriteResult delete(final String kind, final Class clazz, final V id) {
    return delete(find(kind, clazz).filter(Mapper.ID_KEY, id));
  }

  public  WriteResult delete(final Class clazz, final V id, final WriteConcern wc) {
    return delete(createQuery(clazz).filter(Mapper.ID_KEY, id), wc);
  }

  public  WriteResult delete(final Class clazz, final V id) {
    return delete(clazz, id, getWriteConcern(clazz));
  }

  public  WriteResult delete(final Class clazz, final Iterable ids) {
    final Query q = find(clazz).disableValidation().filter(Mapper.ID_KEY + " in", ids);
    return delete(q);
  }

  public  WriteResult delete(final T entity) {
    return delete(entity, getWriteConcern(entity));
  }

  public  WriteResult delete(final T entity, final WriteConcern wc) {
    final T wrapped = ProxyHelper.unwrap(entity);
    if (wrapped instanceof Class) {
      throw new MappingException("Did you mean to delete all documents? -- delete(ds.createQuery(???.class))");
    }
    try {
      final Object id = getId(wrapped);
      return delete(wrapped.getClass(), id, wc);

    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public  WriteResult delete(final Query query) {
    return delete(query, getWriteConcern(query.getEntityClass()));
  }

  public  WriteResult delete(final Query query, final WriteConcern wc) {
    final QueryImpl q = (QueryImpl) query;

    DBCollection dbColl = q.getCollection();
    //TODO remove this after testing.
    if (dbColl == null) {
      dbColl = getCollection(q.getEntityClass());
    }

    final WriteResult wr;

    if (q.getSortObject() != null || q.getOffset() != 0 || q.getLimit() > 0) {
      throw new QueryException("Delete does not allow sort/offset/limit query options.");
    }

    if (q.getQueryObject() != null) {
      if (wc == null) {
        wr = dbColl.remove(q.getQueryObject());
      } else {
        wr = dbColl.remove(q.getQueryObject(), wc);
      }
    } else if (wc == null) {
      wr = dbColl.remove(new BasicDBObject());
    } else {
      wr = dbColl.remove(new BasicDBObject(), wc);
    }

    throwOnError(wc, wr);

    return wr;
  }

  public  void ensureIndex(final Class type, final String fields) {
    ensureIndex(type, null, fields, false, false);
  }

  public  void ensureIndex(final Class clazz, final String name, final IndexFieldDef[] definitions, final boolean unique,
    final boolean dropDupsOnCreate) {
    ensureIndex(clazz, name, definitions, unique, dropDupsOnCreate, false);
  }

  public  void ensureIndex(final Class clazz, final String name, final String fields, final boolean unique,
    final boolean dropDupsOnCreate) {
    ensureIndex(clazz, name, QueryImpl.parseFieldsString(fields, clazz, mapper, true), unique, dropDupsOnCreate, false, false);
  }

  public  void ensureIndex(final Class clazz, final String name, final String fields, final boolean unique,
    final boolean dropDupsOnCreate, final boolean background) {
    ensureIndex(clazz, name, QueryImpl.parseFieldsString(fields, clazz, mapper, true), unique, dropDupsOnCreate, background, false);
  }

  protected  void ensureIndex(final Class clazz, final String name, final BasicDBObject fields, final boolean unique,
    final boolean dropDupsOnCreate, final boolean background, final boolean sparse) {
    final BasicDBObjectBuilder keyOpts = new BasicDBObjectBuilder();
    if (name != null && name.length() != 0) {
      keyOpts.add("name", name);
    }
    if (unique) {
      keyOpts.add("unique", true);
      if (dropDupsOnCreate) {
        keyOpts.add("dropDups", true);
      }
    }

    if (background) {
      keyOpts.add("background", true);
    }
    if (sparse) {
      keyOpts.add("sparse", true);
    }

    final DBCollection dbColl = getCollection(clazz);

    final BasicDBObject opts = (BasicDBObject) keyOpts.get();
    if (opts.isEmpty()) {
      LOG.debug("Ensuring index for " + dbColl.getName() + " with keys:" + fields);
      dbColl.ensureIndex(fields);
    } else {
      LOG.debug("Ensuring index for " + dbColl.getName() + " with keys:" + fields + " and opts:" + opts);
      dbColl.ensureIndex(fields, opts);
    }
  }

  @SuppressWarnings({"rawtypes"})
  public void ensureIndex(final Class clazz, final String name, final IndexFieldDef[] definitions, final boolean unique,
    final boolean dropDupsOnCreate, final boolean background) {
    final BasicDBObjectBuilder keys = BasicDBObjectBuilder.start();

    for (final IndexFieldDef def : definitions) {
      final String fieldName = def.getField();
      final IndexDirection dir = def.getDirection();
      keys.add(fieldName, dir.toIndexValue());
    }

    ensureIndex(clazz, name, (BasicDBObject) keys.get(), unique, dropDupsOnCreate, background, false);
  }

  public  void ensureIndex(final Class type, final String name, final IndexDirection dir) {
    ensureIndex(type, new IndexFieldDef(name, dir));
  }

  public  void ensureIndex(final Class type, final IndexFieldDef... fields) {
    ensureIndex(type, null, fields, false, false);
  }

  public  void ensureIndex(final Class type, final boolean background, final IndexFieldDef... fields) {
    ensureIndex(type, null, fields, false, false, background);
  }

  protected void ensureIndexes(final MappedClass mc, final boolean background) {
    ensureIndexes(mc, background, new ArrayList(), new ArrayList());
  }

  protected void ensureIndexes(final MappedClass mc, final boolean background, final ArrayList parentMCs,
    final ArrayList parentMFs) {
    if (parentMCs.contains(mc)) {
      return;
    }

    //skip embedded types
    if (mc.getEmbeddedAnnotation() != null && (parentMCs == null || parentMCs.isEmpty())) {
      return;
    }

    //Ensure indexes from class annotation
    final ArrayList indexes = mc.getAnnotations(Indexes.class);
    if (indexes != null) {
      for (final Annotation ann : indexes) {
        final Indexes idx = (Indexes) ann;
        if (idx != null && idx.value() != null && idx.value().length > 0) {
          for (final Index index : idx.value()) {
            final BasicDBObject fields = QueryImpl.parseFieldsString(index.value(), mc.getClazz(), mapper, !index.disableValidation());
            ensureIndex(mc.getClazz(), index.name(), fields, index.unique(), index.dropDups(),
              index.background() ? index.background() : background, index.sparse());
          }
        }
      }
    }
    //Ensure indexes from field annotations, and embedded entities
    for (final MappedField mf : mc.getPersistenceFields()) {
      if (mf.hasAnnotation(Indexed.class)) {
        final Indexed index = mf.getAnnotation(Indexed.class);
        final StringBuilder field = new StringBuilder();
        final Class indexedClass = (parentMCs.isEmpty() ? mc : parentMCs.get(0)).getClazz();
        if (!parentMCs.isEmpty()) {
          for (final MappedField pmf : parentMFs) {
            field.append(pmf.getNameToStore()).append(".");
          }
        }

        field.append(mf.getNameToStore());

        ensureIndex(indexedClass, index.name(), new BasicDBObject(field.toString(), index.value().toIndexValue()), index.unique(),
          index.dropDups(), index.background() ? index.background() : background, index.sparse());
      }

      if (!mf.isTypeMongoCompatible() && !mf.hasAnnotation(Reference.class) && !mf.hasAnnotation(Serialized.class)) {
        final ArrayList newParentClasses = (ArrayList) parentMCs.clone();
        final ArrayList newParents = (ArrayList) parentMFs.clone();
        newParentClasses.add(mc);
        newParents.add(mf);
        ensureIndexes(mapper.getMappedClass(mf.isSingleValue() ? mf.getType() : mf.getSubClass()), background, newParentClasses, newParents);
      }
    }
  }

  public  void ensureIndexes(final Class clazz) {
    ensureIndexes(clazz, false);
  }

  public  void ensureIndexes(final Class clazz, final boolean background) {
    final MappedClass mc = mapper.getMappedClass(clazz);
    ensureIndexes(mc, background);
  }

  public void ensureIndexes() {
    ensureIndexes(false);
  }

  public void ensureIndexes(final boolean background) {
    // loops over mappedClasses and call ensureIndex for each @Entity object
    // (for now)
    for (final MappedClass mc : mapper.getMappedClasses()) {
      ensureIndexes(mc, background);
    }
  }


  public void ensureCaps() {
    for (final MappedClass mc : mapper.getMappedClasses()) {
      if (mc.getEntityAnnotation() != null && mc.getEntityAnnotation().cap().value() > 0) {
        final CappedAt cap = mc.getEntityAnnotation().cap();
        final String collName = mapper.getCollectionName(mc.getClazz());
        final BasicDBObjectBuilder dbCapOpts = BasicDBObjectBuilder.start("capped", true);
        if (cap.value() > 0) {
          dbCapOpts.add("size", cap.value());
        }
        if (cap.count() > 0) {
          dbCapOpts.add("max", cap.count());
        }
        final DB database = getDB();
        if (database.getCollectionNames().contains(collName)) {
          final DBObject dbResult = database.command(BasicDBObjectBuilder.start("collstats", collName).get());
          if (dbResult.containsField("capped")) {
            // TODO: check the cap options.
            LOG.warning("DBCollection already exists is capped already; doing nothing. " + dbResult);
          } else {
            LOG.warning("DBCollection already exists with same name(" + collName + ") and is not capped; not creating capped version!");
          }
        } else {
          getDB().createCollection(collName, dbCapOpts.get());
          LOG.debug("Created capped DBCollection (" + collName + ") with opts " + dbCapOpts);
        }
      }
    }
  }

  public  Query queryByExample(final T ex) {
    return queryByExample(getCollection(ex), ex);
  }

  public  Query queryByExample(final String kind, final T ex) {
    return queryByExample(db.getCollection(kind), ex);
  }

  private  Query queryByExample(final DBCollection coll, final T example) {
    //TODO: think about remove className from baseQuery param below.
    return new QueryImpl((Class) example.getClass(), coll, this, entityToDBObj(example, new HashMap()));

  }

  public  Query createQuery(final Class clazz) {
    return new QueryImpl(clazz, getCollection(clazz), this);
  }

  public  Query createQuery(final Class kind, final DBObject q) {
    return new QueryImpl(kind, getCollection(kind), this, q);
  }

  public  Query createQuery(final String kind, final Class clazz, final DBObject q) {
    return new QueryImpl(clazz, db.getCollection(kind), this, q);
  }

  public  Query createQuery(final String kind, final Class clazz) {
    return new QueryImpl(clazz, db.getCollection(kind), this);
  }

  public  Query find(final String kind, final Class clazz) {
    return new QueryImpl(clazz, getCollection(kind), this);
  }


  public  Query find(final Class clazz) {
    return createQuery(clazz);
  }


  public  Query find(final Class clazz, final String property, final V value) {
    final Query query = createQuery(clazz);
    return query.filter(property, value);
  }


  public  Query find(final String kind, final Class clazz, final String property, final V value, final int offset,
    final int size) {
    return find(kind, clazz, property, value, offset, size, true);
  }

  public  Query find(final String kind, final Class clazz, final String property, final V value, final int offset,
    final int size, final boolean validate) {
    final Query query = find(kind, clazz);
    if (!validate) {
      query.disableValidation();
    }
    query.offset(offset);
    query.limit(size);
    return query.filter(property, value).enableValidation();
  }


  public  Query find(final Class clazz, final String property, final V value, final int offset, final int size) {
    final Query query = createQuery(clazz);
    query.offset(offset);
    query.limit(size);
    return query.filter(property, value);
  }


  public  T get(final Class clazz, final DBRef ref) {
    return (T) mapper.fromDBObject(clazz, ref.fetch(), createCache());
  }


  public  Query get(final Class clazz, final Iterable ids) {
    return find(clazz).disableValidation().filter(Mapper.ID_KEY + " in", ids).enableValidation();
  }

  /**
   * Queries the server to check for each DBRef
   */
  public  List> getKeysByRefs(final List refs) {
    final ArrayList> tempKeys = new ArrayList>(refs.size());

    final Map> kindMap = new HashMap>();
    for (final DBRef ref : refs) {
      if (kindMap.containsKey(ref.getRef())) {
        kindMap.get(ref.getRef()).add(ref);
      } else {
        kindMap.put(ref.getRef(), new ArrayList(Collections.singletonList(ref)));
      }
    }
    for (final String kind : kindMap.keySet()) {
      final List objIds = new ArrayList();
      final List kindRefs = kindMap.get(kind);
      for (final DBRef key : kindRefs) {
        objIds.add(key.getId());
      }
      final List> kindResults = this.find(kind, null).disableValidation().filter("_id in", objIds).asKeyList();
      tempKeys.addAll(kindResults);
    }

    //put them back in order, minus the missing ones.
    final ArrayList> keys = new ArrayList>(refs.size());
    for (final DBRef ref : refs) {
      final Key testKey = mapper.refToKey(ref);
      if (tempKeys.contains(testKey)) {
        keys.add(testKey);
      }
    }
    return keys;
  }

  public  List getByKeys(final Iterable> keys) {
    return getByKeys(null, keys);
  }

  @SuppressWarnings("rawtypes")
  public  List getByKeys(final Class clazz, final Iterable> keys) {

    final Map> kindMap = new HashMap>();
    final List entities = new ArrayList();
    // String clazzKind = (clazz==null) ? null :
    // getMapper().getCollectionName(clazz);
    for (final Key key : keys) {
      mapper.updateKind(key);

      // if (clazzKind != null && !key.getKind().equals(clazzKind))
      // throw new IllegalArgumentException("Types are not equal (" +
      // clazz + "!=" + key.getKindClass() +
      // ") for key and method parameter clazz");
      //
      if (kindMap.containsKey(key.getKind())) {
        kindMap.get(key.getKind()).add(key);
      } else {
        kindMap.put(key.getKind(), new ArrayList(Collections.singletonList((Key) key)));
      }
    }
    for (final String kind : kindMap.keySet()) {
      final List objIds = new ArrayList();
      final List kindKeys = kindMap.get(kind);
      for (final Key key : kindKeys) {
        objIds.add(key.getId());
      }
      final List kindResults = find(kind, null).disableValidation().filter("_id in", objIds).asList();
      entities.addAll(kindResults);
    }

    //TODO: order them based on the incoming Keys.
    return entities;
  }


  public  T get(final String kind, final Class clazz, final V id) {
    final List results = find(kind, clazz, Mapper.ID_KEY, id, 0, 1).asList();
    if (results == null || results.isEmpty()) {
      return null;
    }
    return results.get(0);
  }


  public  T get(final Class clazz, final V id) {
    return find(getCollection(clazz).getName(), clazz, Mapper.ID_KEY, id, 0, 1, true).get();
  }


  public  T getByKey(final Class clazz, final Key key) {
    final String kind = mapper.getCollectionName(clazz);
    final String keyKind = mapper.updateKind(key);
    if (!kind.equals(keyKind)) {
      throw new RuntimeException("collection names don't match for key and class: " + kind + " != " + keyKind);
    }

    return get(clazz, key.getId());
  }

  public  T get(final T entity) {
    final T unwrapped = ProxyHelper.unwrap(entity);
    final Object id = getId(unwrapped);
    if (id == null) {
      throw new MappingException("Could not get id for " + unwrapped.getClass().getName());
    }
    return (T) get(unwrapped.getClass(), id);
  }

  public Key exists(final Object entityOrKey) {
    final Object unwrapped = ProxyHelper.unwrap(entityOrKey);
    final Key key = getKey(unwrapped);
    final Object id = key.getId();
    if (id == null) {
      throw new MappingException("Could not get id for " + unwrapped.getClass().getName());
    }

    String collName = key.getKind();
    if (collName == null) {
      collName = getCollection(key.getKindClass()).getName();
    }

    return find(collName, key.getKindClass()).filter(Mapper.ID_KEY, key.getId()).getKey();
  }

  @SuppressWarnings("rawtypes")
  public DBCollection getCollection(final Class clazz) {
    final String collName = mapper.getCollectionName(clazz);
    return getDB().getCollection(collName);
  }

  public DBCollection getCollection(final Object obj) {
    if (obj == null) {
      return null;
    }
    return getCollection(obj.getClass());
  }

  protected DBCollection getCollection(final String kind) {
    if (kind == null) {
      return null;
    }
    return getDB().getCollection(kind);
  }

  public  long getCount(final T entity) {
    return getCollection(ProxyHelper.unwrap(entity)).count();
  }


  public  long getCount(final Class clazz) {
    return getCollection(clazz).count();
  }


  public long getCount(final String kind) {
    return getCollection(kind).count();
  }


  public  long getCount(final Query query) {
    return query.countAll();
  }


  public Mongo getMongo() {
    return mongo;
  }


  public DB getDB() {
    return db;
  }

  public Mapper getMapper() {
    return mapper;
  }

  public  Iterable> insert(final Iterable entities) {
    //TODO: try not to create two iterators...
    final Object first = entities.iterator().next();
    return insert(entities, getWriteConcern(first));
  }

  public  Iterable> insert(final String kind, final Iterable entities, final WriteConcern wc) {
    final DBCollection dbColl = db.getCollection(kind);
    return insert(dbColl, entities, wc);
  }

  public  Iterable> insert(final String kind, final Iterable entities) {
    return insert(kind, entities, getWriteConcern(entities.iterator().next()));
  }


  public  Iterable> insert(final Iterable entities, final WriteConcern wc) {
    //TODO: Do this without creating another iterator
    final DBCollection dbColl = getCollection(entities.iterator().next());
    return insert(dbColl, entities, wc);
  }

  private  Iterable> insert(final DBCollection dbColl, final Iterable entities, final WriteConcern wc) {
    final ArrayList list = entities instanceof List
      ? new ArrayList(((List) entities).size())
      : new ArrayList();

    final Map involvedObjects = new LinkedHashMap();
    for (final T ent : entities) {
      final MappedClass mc = mapper.getMappedClass(ent);
      if (mc.getAnnotation(NotSaved.class) != null) {
        throw new MappingException(
          "Entity type: " + mc.getClazz().getName() + " is marked as NotSaved which means you should not try to save it!");
      }
      list.add(entityToDBObj(ent, involvedObjects));
    }

    final WriteResult wr = null;

    final DBObject[] dbObjects = new DBObject[list.size()];
    dbColl.insert(list.toArray(dbObjects), wc);

    throwOnError(wc, wr);

    final ArrayList> savedKeys = new ArrayList>();
    final Iterator entitiesIT = entities.iterator();
    final Iterator dbObjectsIT = list.iterator();

    while (entitiesIT.hasNext()) {
      final T entity = entitiesIT.next();
      final DBObject dbObj = dbObjectsIT.next();
      savedKeys.add(postSaveGetKey(entity, dbObj, dbColl, involvedObjects));
    }

    return savedKeys;
  }

  public  Iterable> insert(final T... entities) {
    return insert(Arrays.asList(entities), getWriteConcern(entities[0]));
  }

  public  Key insert(final T entity) {
    return insert(entity, getWriteConcern(entity));
  }

  public  Key insert(final T entity, final WriteConcern wc) {
    final T unwrapped = ProxyHelper.unwrap(entity);
    final DBCollection dbColl = getCollection(unwrapped);
    return insert(dbColl, unwrapped, wc);
  }

  public  Key insert(final String kind, final T entity) {
    final T unwrapped = ProxyHelper.unwrap(entity);
    final DBCollection dbColl = getCollection(kind);
    return insert(dbColl, unwrapped, getWriteConcern(unwrapped));
  }

  public  Key insert(final String kind, final T entity, final WriteConcern wc) {
    final T unwrapped = ProxyHelper.unwrap(entity);
    final DBCollection dbColl = getCollection(kind);
    return insert(dbColl, unwrapped, wc);
  }

  protected  Key insert(final DBCollection dbColl, final T entity, final WriteConcern wc) {
    final LinkedHashMap involvedObjects = new LinkedHashMap();
    final DBObject dbObj = entityToDBObj(entity, involvedObjects);
    final WriteResult wr;
    if (wc == null) {
      wr = dbColl.insert(dbObj);
    } else {
      wr = dbColl.insert(dbObj, wc);
    }

    throwOnError(wc, wr);

    return postSaveGetKey(entity, dbObj, dbColl, involvedObjects);

  }

  protected DBObject entityToDBObj(final Object entity, final Map involvedObjects) {
    return mapper.toDBObject(ProxyHelper.unwrap(entity), involvedObjects);
  }

  /**
   * call postSaveOperations and returns Key for entity
   */
  protected  Key postSaveGetKey(final T entity, final DBObject dbObj, final DBCollection dbColl,
    final Map involvedObjects) {
    if (dbObj.get(Mapper.ID_KEY) == null) {
      throw new MappingException("Missing _id after save!");
    }

    postSaveOperations(entity, dbObj, involvedObjects);
    final Key key = new Key(dbColl.getName(), getId(entity));
    key.setKindClass((Class) entity.getClass());

    return key;
  }

  public  Iterable> save(final Iterable entities) {
    Object first = null;
    try {
      first = entities.iterator().next();
    } catch (Exception e) {
      //do nothing
    }
    return save(entities, getWriteConcern(first));
  }

  public  Iterable> save(final Iterable entities, final WriteConcern wc) {
    final ArrayList> savedKeys = new ArrayList>();
    for (final T ent : entities) {
      savedKeys.add(save(ent, wc));
    }
    return savedKeys;

  }

  public  Iterable> save(final T... entities) {
    final ArrayList> savedKeys = new ArrayList>();
    for (final T ent : entities) {
      savedKeys.add(save(ent));
    }
    return savedKeys;
  }

  protected  Key save(final DBCollection dbColl, final T entity, final WriteConcern wc) {
    final MappedClass mc = mapper.getMappedClass(entity);
    if (mc.getAnnotation(NotSaved.class) != null) {
      throw new MappingException(
        "Entity type: " + mc.getClazz().getName() + " is marked as NotSaved which means you should not try to save it!");
    }

    WriteResult wr;

    //involvedObjects is used not only as a cache but also as a list of what needs to be called for life-cycle methods at the end.
    final LinkedHashMap involvedObjects = new LinkedHashMap();
    final DBObject dbObj = entityToDBObj(entity, involvedObjects);

    //try to do an update if there is a @Version field
    wr = tryVersionedUpdate(dbColl, entity, dbObj, wc, db, mc);

    if (wr == null) {
      if (wc == null) {
        wr = dbColl.save(dbObj);
      } else {
        wr = dbColl.save(dbObj, wc);
      }
    }

    throwOnError(wc, wr);
    return postSaveGetKey(entity, dbObj, dbColl, involvedObjects);
  }

  protected  WriteResult tryVersionedUpdate(final DBCollection dbColl, final T entity, final DBObject dbObj, final WriteConcern wc,
    final DB database, final MappedClass mc) {
    WriteResult wr = null;
    if (mc.getFieldsAnnotatedWith(Version.class).isEmpty()) {
      return wr;
    }

    final MappedField mfVersion = mc.getFieldsAnnotatedWith(Version.class).get(0);
    final String versionKeyName = mfVersion.getNameToStore();
    final Long oldVersion = (Long) mfVersion.getFieldValue(entity);
    final long newVersion = VersionHelper.nextValue(oldVersion);
    dbObj.put(versionKeyName, newVersion);
    if (oldVersion != null && oldVersion > 0) {
      final Object idValue = dbObj.get(Mapper.ID_KEY);

      final UpdateResults res = update(find(dbColl.getName(), (Class) entity.getClass()).filter(Mapper.ID_KEY, idValue).filter(
        versionKeyName, oldVersion), dbObj, false, false, wc);

      wr = res.getWriteResult();

      if (res.getUpdatedCount() != 1) {
        throw new ConcurrentModificationException(
          "Entity of class " + entity.getClass().getName() + " (id='" + idValue + "',version='" + oldVersion
            + "') was concurrently updated.");
      }
    } else if (wc == null) {
      wr = dbColl.save(dbObj);
    } else {
      wr = dbColl.save(dbObj, wc);
    }

    //update the version.
    mfVersion.setFieldValue(entity, newVersion);
    return wr;
  }

  protected void throwOnError(final WriteConcern wc, final WriteResult wr) {
    if (wc == null && wr.getLastConcern() == null) {
      final CommandResult cr = wr.getLastError();
      if (cr != null && cr.getErrorMessage() != null && cr.getErrorMessage().length() != 0) {
        cr.throwOnError();
      }
    }
  }

  public  Key save(final String kind, final T entity) {
    final T unwrapped = ProxyHelper.unwrap(entity);
    final DBCollection dbColl = getCollection(kind);
    return save(dbColl, unwrapped, getWriteConcern(unwrapped));
  }

  public  Key save(final T entity) {
    return save(entity, getWriteConcern(entity));
  }

  public  Key save(final T entity, final WriteConcern wc) {
    final T unwrapped = ProxyHelper.unwrap(entity);
    final DBCollection dbColl = getCollection(unwrapped);
    return save(dbColl, unwrapped, wc);
  }

  public  UpdateOperations createUpdateOperations(final Class clazz) {
    return new UpdateOpsImpl(clazz, getMapper());
  }

  public  UpdateOperations createUpdateOperations(final Class kind, final DBObject ops) {
    final UpdateOpsImpl upOps = (UpdateOpsImpl) createUpdateOperations(kind);
    upOps.setOps(ops);
    return upOps;
  }

  public  UpdateResults update(final Query query, final UpdateOperations ops, final boolean createIfMissing) {
    return update(query, ops, createIfMissing, getWriteConcern(query.getEntityClass()));
  }

  public  UpdateResults update(final Query query, final UpdateOperations ops, final boolean createIfMissing,
    final WriteConcern wc) {
    return update(query, ops, createIfMissing, true, wc);
  }

  public  UpdateResults update(final T ent, final UpdateOperations ops) {
    if (ent instanceof Query) {
      return update((Query) ent, ops);
    }

    final MappedClass mc = mapper.getMappedClass(ent);
    final Query q = (Query) createQuery(mc.getClazz());
    q.disableValidation().filter(Mapper.ID_KEY, getId(ent));

    if (!mc.getFieldsAnnotatedWith(Version.class).isEmpty()) {
      final MappedField versionMF = mc.getFieldsAnnotatedWith(Version.class).get(0);
      final Long oldVer = (Long) versionMF.getFieldValue(ent);
      q.filter(versionMF.getNameToStore(), oldVer);
      ops.set(versionMF.getNameToStore(), VersionHelper.nextValue(oldVer));
    }

    return update(q, ops);
  }

  public  UpdateResults update(final Key key, final UpdateOperations ops) {
    Class clazz = (Class) key.getKindClass();
    if (clazz == null) {
      clazz = (Class) mapper.getClassFromKind(key.getKind());
    }
    return updateFirst(createQuery(clazz).disableValidation().filter(Mapper.ID_KEY, key.getId()), ops);
  }

  public  UpdateResults update(final Query query, final UpdateOperations ops) {
    return update(query, ops, false, true);
  }


  public  UpdateResults updateFirst(final Query query, final UpdateOperations ops) {
    return update(query, ops, false, false);
  }

  public  UpdateResults updateFirst(final Query query, final UpdateOperations ops, final boolean createIfMissing) {
    return update(query, ops, createIfMissing, getWriteConcern(query.getEntityClass()));

  }

  public  UpdateResults updateFirst(final Query query, final UpdateOperations ops, final boolean createIfMissing,
    final WriteConcern wc) {
    return update(query, ops, createIfMissing, false, wc);
  }

  public  UpdateResults updateFirst(final Query query, final T entity, final boolean createIfMissing) {
    final LinkedHashMap involvedObjects = new LinkedHashMap();
    final DBObject dbObj = mapper.toDBObject(entity, involvedObjects);

    final UpdateResults res = update(query, dbObj, createIfMissing, false, getWriteConcern(entity));

    //update _id field
    final CommandResult gle = res.getWriteResult().getCachedLastError();
    if (gle != null && res.getInsertedCount() > 0) {
      dbObj.put(Mapper.ID_KEY, res.getNewId());
    }

    postSaveOperations(entity, dbObj, involvedObjects);
    return res;
  }

  public  Key merge(final T entity) {
    return merge(entity, getWriteConcern(entity));
  }

  public  Key merge(final T entity, final WriteConcern wc) {
    T unwrapped = entity;
    final LinkedHashMap involvedObjects = new LinkedHashMap();
    final DBObject dbObj = mapper.toDBObject(unwrapped, involvedObjects);
    final Key key = getKey(unwrapped);
    unwrapped = ProxyHelper.unwrap(unwrapped);
    final Object id = getId(unwrapped);
    if (id == null) {
      throw new MappingException("Could not get id for " + unwrapped.getClass().getName());
    }

    //remove (immutable) _id field for update.
    dbObj.removeField(Mapper.ID_KEY);

    WriteResult wr;

    final MappedClass mc = mapper.getMappedClass(unwrapped);
    final DBCollection dbColl = getCollection(unwrapped);

    //try to do an update if there is a @Version field
    wr = tryVersionedUpdate(dbColl, unwrapped, dbObj, wc, db, mc);

    if (wr == null) {
      final Query query = (Query) createQuery(unwrapped.getClass()).filter(Mapper.ID_KEY, id);
      wr = update(query, new BasicDBObject("$set", dbObj), false, false, wc).getWriteResult();
    }

    final UpdateResults res = new UpdateResults(wr);

    throwOnError(wc, wr);

    //check for updated count if we have a gle
    final CommandResult gle = wr.getCachedLastError();
    if (gle != null && res.getUpdatedCount() == 0) {
      throw new UpdateException("Not updated: " + gle);
    }

    postSaveOperations(unwrapped, dbObj, involvedObjects);
    return key;
  }

  private  void postSaveOperations(final Object entity, final DBObject dbObj, final Map involvedObjects) {
    mapper.updateKeyInfo(entity, dbObj, createCache());

    //call PostPersist on all involved entities (including the entity)
    for (final Map.Entry e : involvedObjects.entrySet()) {
      final Object ent = e.getKey();
      final DBObject dbO = e.getValue();
      final MappedClass mc = mapper.getMappedClass(ent);
      mc.callLifecycleMethods(PostPersist.class, ent, dbO, mapper);
    }
  }

  @SuppressWarnings("rawtypes")
  private  UpdateResults update(final Query query, final UpdateOperations ops, final boolean createIfMissing, final boolean multi,
    final WriteConcern wc) {
    final DBObject u = ((UpdateOpsImpl) ops).getOps();
    if (((UpdateOpsImpl) ops).isIsolated()) {
      final Query q = query.clone();
      q.disableValidation().filter("$atomic", true);
      return update(q, u, createIfMissing, multi, wc);
    }
    return update(query, u, createIfMissing, multi, wc);
  }

  @SuppressWarnings("rawtypes")
  private  UpdateResults update(final Query query, final UpdateOperations ops, final boolean createIfMissing,
    final boolean multi) {
    return update(query, ops, createIfMissing, multi, getWriteConcern(query.getEntityClass()));
  }

  private  UpdateResults update(final Query query, final DBObject u, final boolean createIfMissing, final boolean multi,
    final WriteConcern wc) {
    final QueryImpl qi = (QueryImpl) query;

    DBCollection dbColl = qi.getCollection();
    //TODO remove this after testing.
    if (dbColl == null) {
      dbColl = getCollection(qi.getEntityClass());
    }

    if (qi.getSortObject() != null && qi.getSortObject().keySet() != null && !qi.getSortObject().keySet().isEmpty()) {
      throw new QueryException("sorting is not allowed for updates.");
    }
    if (qi.getOffset() > 0) {
      throw new QueryException("a query offset is not allowed for updates.");
    }
    if (qi.getLimit() > 0) {
      throw new QueryException("a query limit is not allowed for updates.");
    }

    DBObject q = qi.getQueryObject();
    if (q == null) {
      q = new BasicDBObject();
    }

    if (LOG.isTraceEnabled()) {
      LOG.trace("Executing update(" + dbColl.getName() + ") for query: " + q + ", ops: " + u + ", multi: " + multi + ", upsert: "
        + createIfMissing);
    }

    final WriteResult wr;
    if (wc == null) {
      wr = dbColl.update(q, u, createIfMissing, multi);
    } else {
      wr = dbColl.update(q, u, createIfMissing, multi, wc);
    }

    throwOnError(wc, wr);

    return new UpdateResults(wr);
  }

  public  T findAndDelete(final Query query) {
    DBCollection dbColl = ((QueryImpl) query).getCollection();
    //TODO remove this after testing.
    if (dbColl == null) {
      dbColl = getCollection(query.getEntityClass());
    }

    final QueryImpl qi = ((QueryImpl) query);
    final EntityCache cache = createCache();

    if (LOG.isTraceEnabled()) {
      LOG.trace("Executing findAndModify(" + dbColl.getName() + ") with delete ...");
    }

    final DBObject result = dbColl.findAndModify(qi.getQueryObject(), qi.getFieldsObject(), qi.getSortObject(), true, null, false, false);

    if (result != null) {
      return (T) mapper.fromDBObject(qi.getEntityClass(), result, cache);
    }

    return null;
  }

  public  T findAndModify(final Query q, final UpdateOperations ops) {
    return findAndModify(q, ops, false);
  }

  public  T findAndModify(final Query query, final UpdateOperations ops, final boolean oldVersion) {
    return findAndModify(query, ops, oldVersion, false);
  }

  public  T findAndModify(final Query query, final UpdateOperations ops, final boolean oldVersion, final boolean createIfMissing) {
    final QueryImpl qi = (QueryImpl) query;

    DBCollection dbColl = qi.getCollection();
    //TODO remove this after testing.
    if (dbColl == null) {
      dbColl = getCollection(qi.getEntityClass());
    }

    if (LOG.isTraceEnabled()) {
      LOG.info("Executing findAndModify(" + dbColl.getName() + ") with update ");
    }
    DBObject res = null;
    try {
      res = dbColl.findAndModify(qi.getQueryObject(), qi.getFieldsObject(), qi.getSortObject(), false, ((UpdateOpsImpl) ops).getOps(),
        !oldVersion, createIfMissing);
    } catch (MongoException e) {
      if (e.getMessage() == null || !e.getMessage().contains("matching")) {
        throw e;
      }
    }

    if (res == null) {
      return null;
    } else {
      return (T) mapper.fromDBObject(qi.getEntityClass(), res, createCache());
    }
  }

  @SuppressWarnings("rawtypes")
  public  MapreduceResults mapReduce(final MapreduceType type, final Query q, final Class outputType,
    final MapReduceCommand baseCommand) {

    Assert.parametersNotNull("map", baseCommand.getMap());
    Assert.parameterNotEmpty(baseCommand.getMap(), "map");
    Assert.parametersNotNull("reduce", baseCommand.getReduce());
    Assert.parameterNotEmpty(baseCommand.getMap(), "reduce");


    if (MapreduceType.INLINE.equals(type)) {
      throw new IllegalArgumentException("Inline map/reduce is not supported.");
    }

    final QueryImpl qi = (QueryImpl) q;
    if (qi.getOffset() != 0 || qi.getFieldsObject() != null) {
      throw new QueryException("mapReduce does not allow the offset/retrievedFields query options.");
    }


    OutputType outType;
    switch (type) {
      case REDUCE:
        outType = OutputType.REDUCE;
        break;
      case MERGE:
        outType = OutputType.MERGE;
        break;
      case INLINE:
        outType = OutputType.INLINE;
        break;
      default:
        outType = OutputType.REPLACE;
        break;
    }

    final DBCollection dbColl = qi.getCollection();

    final MapReduceCommand cmd = new MapReduceCommand(dbColl, baseCommand.getMap(), baseCommand.getReduce(), baseCommand.getOutputTarget(),
      outType, qi.getQueryObject());
    cmd.setFinalize(baseCommand.getFinalize());
    cmd.setScope(baseCommand.getScope());

    if (qi.getLimit() > 0) {
      cmd.setLimit(qi.getLimit());
    }
    if (qi.getSortObject() != null) {
      cmd.setSort(qi.getSortObject());
    }

    if (LOG.isTraceEnabled()) {
      LOG.info("Executing " + cmd.toString());
    }

    final MapReduceOutput mpo = dbColl.mapReduce(baseCommand);
    final MapreduceResults mrRes = (MapreduceResults) mapper.fromDBObject(MapreduceResults.class, mpo.getRaw(), createCache());

    QueryImpl baseQ = null;
    if (!MapreduceType.INLINE.equals(type)) {
      baseQ = new QueryImpl(outputType, db.getCollection(mrRes.getOutputCollectionName()), this);
    }
    //TODO Handle inline case and create an iterator/able.

    mrRes.setBits(type, baseQ);
    return mrRes;

  }

  @SuppressWarnings("rawtypes")
  public  MapreduceResults mapReduce(final MapreduceType type, final Query query, final String map, final String reduce,
    final String finalize, final Map scopeFields, final Class outputType) {

    final QueryImpl qi = (QueryImpl) query;
    final DBCollection dbColl = qi.getCollection();

    final String outColl = mapper.getCollectionName(outputType);

    OutputType outType;
    switch (type) {
      case REDUCE:
        outType = OutputType.REDUCE;
        break;
      case MERGE:
        outType = OutputType.MERGE;
        break;
      case INLINE:
        outType = OutputType.INLINE;
        break;
      default:
        outType = OutputType.REPLACE;
        break;
    }

    final MapReduceCommand cmd = new MapReduceCommand(dbColl, map, reduce, outColl, outType, qi.getQueryObject());

    if (qi.getLimit() > 0) {
      cmd.setLimit(qi.getLimit());
    }
    if (qi.getSortObject() != null) {
      cmd.setSort(qi.getSortObject());
    }

    if (finalize != null && finalize.length() != 0) {
      cmd.setFinalize(finalize);
    }

    if (scopeFields != null && !scopeFields.isEmpty()) {
      cmd.setScope(scopeFields);
    }

    return mapReduce(type, query, outputType, cmd);
  }

  /**
   * Converts a list of keys to refs
   */
  public static  List keysAsRefs(final List> keys, final Mapper mapper) {
    final ArrayList refs = new ArrayList(keys.size());
    for (final Key key : keys) {
      refs.add(mapper.keyToRef(key));
    }
    return refs;
  }

  /**
   * Converts a list of refs to keys
   */
  public static  List> refsToKeys(final Mapper mapper, final List refs, final Class c) {
    final ArrayList> keys = new ArrayList>(refs.size());
    for (final DBRef ref : refs) {
      keys.add((Key) mapper.refToKey(ref));
    }
    return keys;
  }

  private EntityCache createCache() {
    return mapper.createEntityCache();
  }

  /**
   * Gets the write concern for entity or returns the default write concern for this datastore
   */
  public WriteConcern getWriteConcern(final Object clazzOrEntity) {
    WriteConcern wc = defConcern;
    if (clazzOrEntity != null) {
      final Entity entityAnn = getMapper().getMappedClass(clazzOrEntity).getEntityAnnotation();
      if (entityAnn != null && entityAnn.concern() != null && entityAnn.concern().length() != 0) {
        wc = WriteConcern.valueOf(entityAnn.concern());
      }
    }

    return wc;
  }

  public WriteConcern getDefaultWriteConcern() {
    return defConcern;
  }

  public void setDefaultWriteConcern(final WriteConcern wc) {
    defConcern = wc;
  }

  public DBDecoderFactory setDecoderFact(final DBDecoderFactory fact) {
    decoderFactory = fact;
    return decoderFactory;
  }

  public DBDecoderFactory getDecoderFact() {
    return decoderFactory != null ? decoderFactory : mongo.getMongoOptions().dbDecoderFactory;
  }
}