public abstract class

DBCollection

extends Object
// DBCollection.java

/**
 *      Copyright (C) 2008 10gen Inc.
 *
 *   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 com.mongodb;

// Mongo
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.bson.types.ObjectId;

/** This class provides a skeleton implementation of a database collection.
 * <p>A typical invocation sequence is thus
 * <blockquote><pre>
 *     Mongo mongo = new Mongo( new DBAddress( "localhost", 127017 ) );
 *     DB db = mongo.getDB( "mydb" );
 *     DBCollection collection = db.getCollection( "test" );
 * </pre></blockquote>
 * @dochub collections
 */
@SuppressWarnings("unchecked")
public abstract class DBCollection {

    /**
     * Saves document(s) to the database.
     * if doc doesn't have an _id, one will be added
     * you can get the _id that was added from doc after the insert
     *
     * @param arr  array of documents to save
     * @param concern the write concern
     * @return
     * @throws MongoException
     * @dochub insert
     */
    public WriteResult insert(DBObject[] arr , WriteConcern concern ) throws MongoException {
        return insert( arr, concern, getDBEncoder());
    }

    /**
     * Saves document(s) to the database.
     * if doc doesn't have an _id, one will be added
     * you can get the _id that was added from doc after the insert
     *
     * @param arr  array of documents to save
     * @param concern the write concern
     * @param encoder the DBEncoder to use
     * @return
     * @throws MongoException
     * @dochub insert
     */
    public abstract WriteResult insert(DBObject[] arr , WriteConcern concern, DBEncoder encoder) throws MongoException;

    /**
     * Inserts a document into the database.
     * if doc doesn't have an _id, one will be added
     * you can get the _id that was added from doc after the insert
     *
     * @param o
     * @param concern the write concern
     * @return
     * @throws MongoException
     * @dochub insert
     */
    public WriteResult insert(DBObject o , WriteConcern concern )
        throws MongoException {
        return insert( new DBObject[]{ o } , concern );
    }

    /**
     * Saves document(s) to the database.
     * if doc doesn't have an _id, one will be added
     * you can get the _id that was added from doc after the insert
     *
     * @param arr  array of documents to save
     * @return
     * @throws MongoException
     * @dochub insert
     */
    public WriteResult insert(DBObject ... arr)
        throws MongoException {
        return insert( arr , getWriteConcern() );
    }

    /**
     * Saves document(s) to the database.
     * if doc doesn't have an _id, one will be added
     * you can get the _id that was added from doc after the insert
     *
     * @param arr  array of documents to save
     * @return
     * @throws MongoException
     * @dochub insert
     */
    public WriteResult insert(WriteConcern concern, DBObject ... arr)
        throws MongoException {
        return insert( arr, concern );
    }

    /**
     * Saves document(s) to the database.
     * if doc doesn't have an _id, one will be added
     * you can get the _id that was added from doc after the insert
     *
     * @param list list of documents to save
     * @return
     * @throws MongoException
     * @dochub insert
     */
    public WriteResult insert(List<DBObject> list )
        throws MongoException {
        return insert( list, getWriteConcern() );
    }

    /**
     * Saves document(s) to the database.
     * if doc doesn't have an _id, one will be added
     * you can get the _id that was added from doc after the insert
     * 
     * @param list list of documents to save
     * @param concern the write concern
     * @return
     * @throws MongoException
     * @dochub insert
     */
    public WriteResult insert(List<DBObject> list, WriteConcern concern )
            throws MongoException {
        return insert( list.toArray( new DBObject[list.size()] ) , concern );
    }

    /**
     * Performs an update operation.
     * @param q search query for old object to update
     * @param o object with which to update <tt>q</tt>
     * @param upsert if the database should create the element if it does not exist
     * @param multi if the update should be applied to all objects matching (db version 1.1.3 and above). An object will
     * not be inserted if it does not exist in the collection and upsert=true and multi=true.
     * See <a href="http://www.mongodb.org/display/DOCS/Atomic+Operations">http://www.mongodb.org/display/DOCS/Atomic+Operations</a>
     * @param concern the write concern
     * @return
     * @throws MongoException
     * @dochub update
     */
    public WriteResult update( DBObject q , DBObject o , boolean upsert , boolean multi , WriteConcern concern ) throws MongoException {
        return update( q, o, upsert, multi, concern, getDBEncoder());
    }

    /**
     * Performs an update operation.
     * @param q search query for old object to update
     * @param o object with which to update <tt>q</tt>
     * @param upsert if the database should create the element if it does not exist
     * @param multi if the update should be applied to all objects matching (db version 1.1.3 and above). An object will
     * not be inserted if it does not exist in the collection and upsert=true and multi=true.
     * See <a href="http://www.mongodb.org/display/DOCS/Atomic+Operations">http://www.mongodb.org/display/DOCS/Atomic+Operations</a>
     * @param concern the write concern
     * @param encoder the DBEncoder to use
     * @return
     * @throws MongoException
     * @dochub update
     */
    public abstract WriteResult update( DBObject q , DBObject o , boolean upsert , boolean multi , WriteConcern concern, DBEncoder encoder ) throws MongoException ;

    /**
     * calls {@link DBCollection#update(com.mongodb.DBObject, com.mongodb.DBObject, boolean, boolean, com.mongodb.WriteConcern)} with default WriteConcern.
     * @param q search query for old object to update
     * @param o object with which to update <tt>q</tt>
     * @param upsert if the database should create the element if it does not exist
     * @param multi if the update should be applied to all objects matching (db version 1.1.3 and above)
     *              See http://www.mongodb.org/display/DOCS/Atomic+Operations
     * @return
     * @throws MongoException
     * @dochub update
     */
    public WriteResult update( DBObject q , DBObject o , boolean upsert , boolean multi )
        throws MongoException {
        return update( q , o , upsert , multi , getWriteConcern() );
    }

    /**
     * calls {@link DBCollection#update(com.mongodb.DBObject, com.mongodb.DBObject, boolean, boolean)} with upsert=false and multi=false
     * @param q search query for old object to update
     * @param o object with which to update <tt>q</tt>
     * @return
     * @throws MongoException
     * @dochub update
     */
    public WriteResult update( DBObject q , DBObject o ) throws MongoException {
        return update( q , o , false , false );
    }

    /**
     * calls {@link DBCollection#update(com.mongodb.DBObject, com.mongodb.DBObject, boolean, boolean)} with upsert=false and multi=true
     * @param q search query for old object to update
     * @param o object with which to update <tt>q</tt>
     * @return
     * @throws MongoException
     * @dochub update
     */
    public WriteResult updateMulti( DBObject q , DBObject o ) throws MongoException {
        return update( q , o , false , true );
    }

    /**
     * Adds any necessary fields to a given object before saving it to the collection.
     * @param o object to which to add the fields
     */
    protected abstract void doapply( DBObject o );

    /**
     * Removes objects from the database collection.
     * @param o the object that documents to be removed must match
     * @param concern WriteConcern for this operation
     * @return
     * @throws MongoException
     * @dochub remove
     */
    public WriteResult remove( DBObject o , WriteConcern concern ) throws MongoException {
        return remove(  o, concern, getDBEncoder());
    }

    /**
     * Removes objects from the database collection.
     * @param o the object that documents to be removed must match
     * @param concern WriteConcern for this operation
     * @param encoder the DBEncoder to use
     * @return
     * @throws MongoException
     * @dochub remove
     */
    public abstract WriteResult remove( DBObject o , WriteConcern concern, DBEncoder encoder ) throws MongoException ;

    /**
     * calls {@link DBCollection#remove(com.mongodb.DBObject, com.mongodb.WriteConcern)} with the default WriteConcern
     * @param o the object that documents to be removed must match
     * @return
     * @throws MongoException
     * @dochub remove
     */
    public WriteResult remove( DBObject o )
        throws MongoException {
        return remove( o , getWriteConcern() );
    }


    /**
     * Finds objects
     */
    abstract Iterator<DBObject> __find( DBObject ref , DBObject fields , int numToSkip , int batchSize , int limit, int options, ReadPreference readPref, DBDecoder decoder ) throws MongoException ;

    abstract Iterator<DBObject> __find( DBObject ref , DBObject fields , int numToSkip , int batchSize , int limit, int options,
                                        ReadPreference readPref, DBDecoder decoder, DBEncoder encoder ) throws MongoException ;


    /**
     * Calls {@link DBCollection#find(com.mongodb.DBObject, com.mongodb.DBObject, int, int)} and applies the query options
     * @param query query used to search
     * @param fields the fields of matching objects to return
     * @param numToSkip number of objects to skip
     * @param batchSize the batch size. This option has a complex behavior, see {@link DBCursor#batchSize(int) }
     * @param options - see Bytes QUERYOPTION_*
     * @return the cursor
     * @throws MongoException
     * @dochub find
     */
    @Deprecated
    public DBCursor find( DBObject query , DBObject fields , int numToSkip , int batchSize , int options ) throws MongoException{
    	return find(query, fields, numToSkip, batchSize).addOption(options);
    }


    /**
     * Finds objects from the database that match a query.
     * A DBCursor object is returned, that can be iterated to go through the results.
     *
     * @param query query used to search
     * @param fields the fields of matching objects to return
     * @param numToSkip number of objects to skip
     * @param batchSize the batch size. This option has a complex behavior, see {@link DBCursor#batchSize(int) }
     * @return the cursor
     * @throws MongoException
     * @dochub find
     */
    @Deprecated
    public DBCursor find( DBObject query , DBObject fields , int numToSkip , int batchSize ) {
    	DBCursor cursor = find(query, fields).skip(numToSkip).batchSize(batchSize);
    	return cursor;
    }

    // ------

    /**
     * Finds an object by its id.
     * This compares the passed in value to the _id field of the document
     *
     * @param obj any valid object
     * @return the object, if found, otherwise <code>null</code>
     * @throws MongoException
     */
    public DBObject findOne( Object obj )
        throws MongoException {
        return findOne(obj, null);
    }


    /**
     * Finds an object by its id.
     * This compares the passed in value to the _id field of the document
     *
     * @param obj any valid object
     * @param fields fields to return
     * @return the object, if found, otherwise <code>null</code>
     * @dochub find
     */
    public DBObject findOne( Object obj, DBObject fields ) {
        Iterator<DBObject> iterator = __find( new BasicDBObject("_id", obj), fields, 0, -1, 0, getOptions(), getReadPreference(), getDecoder() );
        return (iterator.hasNext() ? iterator.next() : null);
    }

    /**
     * Finds the first document in the query and updates it.
     * @param query query to match
     * @param fields fields to be returned
     * @param sort sort to apply before picking first document
     * @param remove if true, document found will be removed
     * @param update update to apply
     * @param returnNew if true, the updated document is returned, otherwise the old document is returned (or it would be lost forever)
     * @param upsert do upsert (insert if document not present)
     * @return the document
     */
    public DBObject findAndModify(DBObject query, DBObject fields, DBObject sort, boolean remove, DBObject update, boolean returnNew, boolean upsert) {

        BasicDBObject cmd = new BasicDBObject( "findandmodify", _name);
        if (query != null && !query.keySet().isEmpty())
            cmd.append( "query", query );
        if (fields != null && !fields.keySet().isEmpty())
            cmd.append( "fields", fields );
        if (sort != null && !sort.keySet().isEmpty())
            cmd.append( "sort", sort );

        if (remove)
            cmd.append( "remove", remove );
        else {
            if (update != null && !update.keySet().isEmpty()) {
                // if 1st key doesnt start with $, then object will be inserted as is, need to check it
                String key = update.keySet().iterator().next();
                if (key.charAt(0) != '$')
                    _checkObject(update, false, false);
                cmd.append( "update", update );
            }
            if (returnNew)
                cmd.append( "new", returnNew );
            if (upsert)
                cmd.append( "upsert", upsert );
        }

        if (remove && !(update == null || update.keySet().isEmpty() || returnNew))
            throw new MongoException("FindAndModify: Remove cannot be mixed with the Update, or returnNew params!");

        CommandResult res = this._db.command( cmd );
        if (res.ok() || res.getErrorMessage().equals( "No matching object found" ))
            return (DBObject) res.get( "value" );
        res.throwOnError();
        return null;
    }


    /**
     * calls {@link DBCollection#findAndModify(com.mongodb.DBObject, com.mongodb.DBObject, com.mongodb.DBObject, boolean, com.mongodb.DBObject, boolean, boolean)}
     * with fields=null, remove=false, returnNew=false, upsert=false
     * @param query
     * @param sort
     * @param update
     * @return the old document
     */
    public DBObject findAndModify( DBObject query , DBObject sort , DBObject update){
    	return findAndModify( query, null, sort, false, update, false, false);
    }

    /**
     * calls {@link DBCollection#findAndModify(com.mongodb.DBObject, com.mongodb.DBObject, com.mongodb.DBObject, boolean, com.mongodb.DBObject, boolean, boolean)}
     * with fields=null, sort=null, remove=false, returnNew=false, upsert=false
     * @param query
     * @param update
     * @return the old document
     */
    public DBObject findAndModify( DBObject query , DBObject update ) {
    	return findAndModify( query, null, null, false, update, false, false );
    }

    /**
     * calls {@link DBCollection#findAndModify(com.mongodb.DBObject, com.mongodb.DBObject, com.mongodb.DBObject, boolean, com.mongodb.DBObject, boolean, boolean)}
     * with fields=null, sort=null, remove=true, returnNew=false, upsert=false
     * @param query
     * @return the removed document
     */
    public DBObject findAndRemove( DBObject query ) {
    	return findAndModify( query, null, null, true, null, false, false );
    }

    // --- START INDEX CODE ---

    /**
     * calls {@link DBCollection#createIndex(com.mongodb.DBObject, com.mongodb.DBObject)} with default index options
     * @param keys an object with a key set of the fields desired for the index
     * @throws MongoException
     */
    public void createIndex( final DBObject keys )
        throws MongoException {
        createIndex( keys , defaultOptions( keys ) );
    }

    /**
     * Forces creation of an index on a set of fields, if one does not already exist.
     * @param keys
     * @param options
     * @throws MongoException
     */
    public void createIndex( DBObject keys , DBObject options ) throws MongoException {
        createIndex( keys, options, getDBEncoder());
    }

    /**
     * Forces creation of an index on a set of fields, if one does not already exist.
     * @param keys
     * @param options
     * @param encoder the DBEncoder to use
     * @throws MongoException
     */
    public abstract void createIndex( DBObject keys , DBObject options, DBEncoder encoder ) throws MongoException;

    /**
     * Creates an ascending index on a field with default options, if one does not already exist.
     * @param name name of field to index on
     */
    public void ensureIndex( final String name ){
        ensureIndex( new BasicDBObject( name , 1 ) );
    }

    /**
     * calls {@link DBCollection#ensureIndex(com.mongodb.DBObject, com.mongodb.DBObject)} with default options
     * @param keys an object with a key set of the fields desired for the index
     * @throws MongoException
     */
    public void ensureIndex( final DBObject keys )
        throws MongoException {
        ensureIndex( keys , defaultOptions( keys ) );
    }

    /**
     * calls {@link DBCollection#ensureIndex(com.mongodb.DBObject, java.lang.String, boolean)} with unique=false
     * @param keys fields to use for index
     * @param name an identifier for the index
     * @throws MongoException
     * @dochub indexes
     */
    public void ensureIndex( DBObject keys , String name )
        throws MongoException {
        ensureIndex( keys , name , false );
    }

    /**
     * Ensures an index on this collection (that is, the index will be created if it does not exist).
     * @param keys fields to use for index
     * @param name an identifier for the index. If null or empty, the default name will be used.
     * @param unique if the index should be unique
     * @throws MongoException
     */
    public void ensureIndex( DBObject keys , String name , boolean unique )
        throws MongoException {
        DBObject options = defaultOptions( keys );
        if (name != null && name.length()>0)
            options.put( "name" , name );
        if ( unique )
            options.put( "unique" , Boolean.TRUE );
        ensureIndex( keys , options );
    }

    /**
     * Creates an index on a set of fields, if one does not already exist.
     * @param keys an object with a key set of the fields desired for the index
     * @param optionsIN options for the index (name, unique, etc)
     * @throws MongoException
     */
    public void ensureIndex( final DBObject keys , final DBObject optionsIN )
        throws MongoException {

        if ( checkReadOnly( false ) ) return;

        final DBObject options = defaultOptions( keys );
        for ( String k : optionsIN.keySet() )
            options.put( k , optionsIN.get( k ) );

        final String name = options.get( "name" ).toString();

        if ( _createdIndexes.contains( name ) )
            return;

        createIndex( keys , options );
        _createdIndexes.add( name );
    }

    /**
     * Clears all indices that have not yet been applied to this collection.
     */
    public void resetIndexCache(){
        _createdIndexes.clear();
    }

    DBObject defaultOptions( DBObject keys ){
        DBObject o = new BasicDBObject();
        o.put( "name" , genIndexName( keys ) );
        o.put( "ns" , _fullName );
        return o;
    }

    /**
     * Convenience method to generate an index name from the set of fields it is over.
     * @param keys the names of the fields used in this index
     * @return a string representation of this index's fields
     */
    public static String genIndexName( DBObject keys ){
        StringBuilder name = new StringBuilder();
        for ( String s : keys.keySet() ){
            if ( name.length() > 0 )
                name.append( '_' );
            name.append( s ).append( '_' );
            Object val = keys.get( s );
            if ( val instanceof Number || val instanceof String )
                name.append( val.toString().replace( ' ', '_' ) );
        }
        return name.toString();
    }

    // --- END INDEX CODE ---

    /**
     * Set hint fields for this collection (to optimize queries).
     * @param lst a list of <code>DBObject</code>s to be used as hints
     */
    public void setHintFields( List<DBObject> lst ){
        _hintFields = lst;
    }

    /**
     * Queries for an object in this collection.
     * @param ref object for which to search
     * @return an iterator over the results
     * @dochub find
     */
    public DBCursor find( DBObject ref ){
        return new DBCursor( this, ref, null, getReadPreference());
    }

    /**
     * Queries for an object in this collection.
     *
     * <p>
     * An empty DBObject will match every document in the collection.
     * Regardless of fields specified, the _id fields are always returned.
     * </p>
     * <p>
     * An example that returns the "x" and "_id" fields for every document
     * in the collection that has an "x" field:
     * </p>
     * <blockquote><pre>
     * BasicDBObject keys = new BasicDBObject();
     * keys.put("x", 1);
     *
     * DBCursor cursor = collection.find(new BasicDBObject(), keys);
     * </pre></blockquote>
     *
     * @param ref object for which to search
     * @param keys fields to return
     * @return a cursor to iterate over results
     * @dochub find
     */
    public DBCursor find( DBObject ref , DBObject keys ){
        return new DBCursor( this, ref, keys, getReadPreference());
    }


    /**
     * Queries for all objects in this collection.
     * @return a cursor which will iterate over every object
     * @dochub find
     */
    public DBCursor find(){
        return new DBCursor( this, null, null, getReadPreference());
    }

    /**
     * Returns a single object from this collection.
     * @return the object found, or <code>null</code> if the collection is empty
     * @throws MongoException
     */
    public DBObject findOne()
        throws MongoException {
        return findOne( new BasicDBObject() );
    }

    /**
     * Returns a single object from this collection matching the query.
     * @param o the query object
     * @return the object found, or <code>null</code> if no such object exists
     * @throws MongoException
     */
    public DBObject findOne( DBObject o )
        throws MongoException {
        return findOne( o, null, getReadPreference());
    }

    /**
     * Returns a single object from this collection matching the query.
     * @param o the query object
     * @param fields fields to return
     * @return the object found, or <code>null</code> if no such object exists
     * @dochub find
     */
    public DBObject findOne( DBObject o, DBObject fields ) {
        return findOne( o, fields, getReadPreference());
    }
    /**
     * Returns a single object from this collection matching the query.
     * @param o the query object
     * @param fields fields to return
     * @return the object found, or <code>null</code> if no such object exists
     * @dochub find
     */
    public DBObject findOne( DBObject o, DBObject fields, ReadPreference readPref ) {
        Iterator<DBObject> i = __find( o , fields , 0 , -1 , 0, getOptions(), readPref, getDecoder() );
        DBObject obj = (i.hasNext() ? i.next() : null);
        if ( obj != null && ( fields != null && fields.keySet().size() > 0 ) ){
            obj.markAsPartialObject();
        }
        return obj;
    }

    // Only create a new decoder if there is a decoder factory explicitly set on the collection.  Otherwise return null
    // so that DBPort will use a cached decoder from the default factory.
    private DBDecoder getDecoder() {
        return getDBDecoderFactory() != null ? getDBDecoderFactory().create() : null;
    }

    // Only create a new encoder if there is an encoder factory explicitly set on the collection.  Otherwise return null
    // to allow DB to create its own or use a cached one.
    private DBEncoder getDBEncoder() {
        return getDBEncoderFactory() != null ? getDBEncoderFactory().create() : null;
    }


    /**
     * calls {@link DBCollection#apply(com.mongodb.DBObject, boolean)} with ensureID=true
     * @param o <code>DBObject</code> to which to add fields
     * @return the modified parameter object
     */
    public Object apply( DBObject o ){
        return apply( o , true );
    }

    /**
     * calls {@link DBCollection#doapply(com.mongodb.DBObject)}, optionally adding an automatic _id field
     * @param jo object to add fields to
     * @param ensureID whether to add an <code>_id</code> field
     * @return the modified object <code>o</code>
     */
    public Object apply( DBObject jo , boolean ensureID ){

        Object id = jo.get( "_id" );
        if ( ensureID && id == null ){
            id = ObjectId.get();
            jo.put( "_id" , id );
        }

        doapply( jo );

        return id;
    }

    /**
     * calls {@link DBCollection#save(com.mongodb.DBObject, com.mongodb.WriteConcern)} with default WriteConcern
     * @param jo the <code>DBObject</code> to save
     *        will add <code>_id</code> field to jo if needed
     * @return
     */
    public WriteResult save( DBObject jo ) {
    	return save(jo, getWriteConcern());
    }

    /**
     * Saves an object to this collection (does insert or update based on the object _id).
     * @param jo the <code>DBObject</code> to save
     * @param concern the write concern
     * @return
     * @throws MongoException
     */
    public WriteResult save( DBObject jo, WriteConcern concern )
        throws MongoException {
        if ( checkReadOnly( true ) )
            return null;

        _checkObject( jo , false , false );

        Object id = jo.get( "_id" );

        if ( id == null || ( id instanceof ObjectId && ((ObjectId)id).isNew() ) ){
            if ( id != null && id instanceof ObjectId )
                ((ObjectId)id).notNew();
            if ( concern == null )
            	return insert( jo );
            else
            	return insert( jo, concern );
        }

        DBObject q = new BasicDBObject();
        q.put( "_id" , id );
        if ( concern == null )
        	return update( q , jo , true , false );
        else
        	return update( q , jo , true , false , concern );

    }

    // ---- DB COMMANDS ----
    /**
     * Drops all indices from this collection
     * @throws MongoException
     */
    public void dropIndexes()
        throws MongoException {
        dropIndexes( "*" );
    }


    /**
     * Drops an index from this collection
     * @param name the index name
     * @throws MongoException
     */
    public void dropIndexes( String name )
        throws MongoException {
        DBObject cmd = BasicDBObjectBuilder.start()
            .add( "deleteIndexes" , getName() )
            .add( "index" , name )
            .get();

        resetIndexCache();
        CommandResult res = _db.command( cmd );
        if (res.ok() || res.getErrorMessage().equals( "ns not found" ))
            return;
        res.throwOnError();
    }

    /**
     * Drops (deletes) this collection. Use with care.
     * @throws MongoException
     */
    public void drop()
        throws MongoException {
        resetIndexCache();
        CommandResult res =_db.command( BasicDBObjectBuilder.start().add( "drop" , getName() ).get() );
        if (res.ok() || res.getErrorMessage().equals( "ns not found" ))
            return;
        res.throwOnError();
    }

    /**
     * returns the number of documents in this collection.
     * @return
     * @throws MongoException
     */
    public long count()
        throws MongoException {
        return getCount(new BasicDBObject(), null);
    }

    /**
     * returns the number of documents that match a query.
     * @param query query to match
     * @return
     * @throws MongoException
     */
    public long count(DBObject query)
        throws MongoException {
        return getCount(query, null);
    }


    /**
     *  calls {@link DBCollection#getCount(com.mongodb.DBObject, com.mongodb.DBObject)} with an empty query and null fields.
     *  @return number of documents that match query
     * @throws MongoException
     */
    public long getCount()
        throws MongoException {
        return getCount(new BasicDBObject(), null);
    }

    /**
     *  calls {@link DBCollection#getCount(com.mongodb.DBObject, com.mongodb.DBObject)} with null fields.
     *  @param query query to match
     *  @return
     * @throws MongoException
     */
    public long getCount(DBObject query)
        throws MongoException {
        return getCount(query, null);
    }

    /**
     *  calls {@link DBCollection#getCount(com.mongodb.DBObject, com.mongodb.DBObject, long, long)} with limit=0 and skip=0
     *  @param query query to match
     *  @param fields fields to return
     *  @return
     * @throws MongoException
     */
    public long getCount(DBObject query, DBObject fields)
        throws MongoException {
        return getCount( query , fields , 0 , 0 );
    }

    /**
     *  Returns the number of documents in the collection
     *  that match the specified query
     *
     *  @param query query to select documents to count
     *  @param fields fields to return
     *  @param limit limit the count to this value
     * @param skip number of entries to skip
     * @return number of documents that match query and fields
     * @throws MongoException
     */
    public long getCount(DBObject query, DBObject fields, long limit, long skip )
        throws MongoException {

        BasicDBObject cmd = new BasicDBObject();
        cmd.put("count", getName());
        cmd.put("query", query);
        if (fields != null) {
            cmd.put("fields", fields);
        }

        if ( limit > 0 )
            cmd.put( "limit" , limit );
        if ( skip > 0 )
            cmd.put( "skip" , skip );

        CommandResult res = _db.command(cmd,getOptions());

        if ( ! res.ok() ){
            String errmsg = res.getErrorMessage();

            if ( errmsg.equals("ns does not exist") ||
                 errmsg.equals("ns missing" ) ){
                // for now, return 0 - lets pretend it does exist
                return 0;
            }

            res.throwOnError();
        }

        return res.getLong("n");
    }

    /**
     * Calls {@link DBCollection#rename(java.lang.String, boolean)} with dropTarget=false
     * @param newName new collection name (not a full namespace)
     * @return the new collection
     * @throws MongoException
     */
    public DBCollection rename( String newName )
        throws MongoException {
        return rename(newName, false);
    }

    /**
     * renames of this collection to newName
     * @param newName new collection name (not a full namespace)
     * @param dropTarget if a collection with the new name exists, whether or not to drop it
     * @return the new collection
     * @throws MongoException
     */
    public DBCollection rename( String newName, boolean dropTarget )
        throws MongoException {
        CommandResult ret =
            _db.getSisterDB( "admin" )
            .command( BasicDBObjectBuilder.start()
                      .add( "renameCollection" , _fullName )
                      .add( "to" , _db._name + "." + newName )
                      .add( "dropTarget" , dropTarget )
                      .get() );
        ret.throwOnError();
        resetIndexCache();
        return _db.getCollection( newName );
    }

    /**
     * calls {@link DBCollection#group(com.mongodb.DBObject, com.mongodb.DBObject, com.mongodb.DBObject, java.lang.String, java.lang.String)} with finalize=null
     * @param key - { a : true }
     * @param cond - optional condition on query
     * @param reduce javascript reduce function
     * @param initial initial value for first match on a key
     * @return
     * @throws MongoException
     * @see <a href="http://www.mongodb.org/display/DOCS/Aggregation">http://www.mongodb.org/display/DOCS/Aggregation</a>
     */
    public DBObject group( DBObject key , DBObject cond , DBObject initial , String reduce )
        throws MongoException {
        return group( key , cond , initial , reduce , null );
    }

    /**
     * Applies a group operation
     * @param key - { a : true }
     * @param cond - optional condition on query
     * @param reduce javascript reduce function
     * @param initial initial value for first match on a key
     * @param finalize An optional function that can operate on the result(s) of the reduce function.
     * @return
     * @throws MongoException
     * @see <a href="http://www.mongodb.org/display/DOCS/Aggregation">http://www.mongodb.org/display/DOCS/Aggregation</a>
     */
    public DBObject group( DBObject key , DBObject cond , DBObject initial , String reduce , String finalize )
        throws MongoException {
        GroupCommand cmd = new GroupCommand(this, key, cond, initial, reduce, finalize);
        return group( cmd );
    }

    /**
     * Applies a group operation
     * @param cmd the group command
     * @return
     * @throws MongoException
     * @see <a href="http://www.mongodb.org/display/DOCS/Aggregation">http://www.mongodb.org/display/DOCS/Aggregation</a>
     */
    public DBObject group( GroupCommand cmd ) {
        CommandResult res =  _db.command( cmd.toDBObject(), getOptions() );
        res.throwOnError();
        return (DBObject)res.get( "retval" );
    }


    /**
     * @deprecated prefer the {@link DBCollection#group(com.mongodb.GroupCommand)} which is more standard
     * Applies a group operation
     * @param args object representing the arguments to the group function
     * @return
     * @throws MongoException
     * @see <a href="http://www.mongodb.org/display/DOCS/Aggregation">http://www.mongodb.org/display/DOCS/Aggregation</a>
     */
    @Deprecated
    public DBObject group( DBObject args )
        throws MongoException {
        args.put( "ns" , getName() );
        CommandResult res =  _db.command( new BasicDBObject( "group" , args ), getOptions() );
        res.throwOnError();
        return (DBObject)res.get( "retval" );
    }

    /**
     * find distinct values for a key
     * @param key
     * @return
     */
    public List distinct( String key ){
        return distinct( key , new BasicDBObject() );
    }

    /**
     * find distinct values for a key
     * @param key
     * @param query query to match
     * @return
     */
	public List distinct( String key , DBObject query ){
        DBObject c = BasicDBObjectBuilder.start()
            .add( "distinct" , getName() )
            .add( "key" , key )
            .add( "query" , query )
            .get();

        CommandResult res = _db.command( c, getOptions() );
        res.throwOnError();
        return (List)(res.get( "values" ));
    }

    /**
     * performs a map reduce operation
     * Runs the command in REPLACE output mode (saves to named collection)
     *
     * @param map
     *            map function in javascript code
     * @param outputTarget
     *            optional - leave null if want to use temp collection
     * @param reduce
     *            reduce function in javascript code
     * @param query
     *            to match
     * @return
     * @throws MongoException
     * @dochub mapreduce
     */
    public MapReduceOutput mapReduce( String map , String reduce , String outputTarget , DBObject query ) throws MongoException{
        return mapReduce( new MapReduceCommand( this , map , reduce , outputTarget , MapReduceCommand.OutputType.REPLACE, query ) );
    }

    /**
     * performs a map reduce operation
     * Specify an outputType to control job execution
     * * INLINE - Return results inline
     * * REPLACE - Replace the output collection with the job output
     * * MERGE - Merge the job output with the existing contents of outputTarget
     * * REDUCE - Reduce the job output with the existing contents of
     * outputTarget
     *
     * @param map
     *            map function in javascript code
     * @param outputTarget
     *            optional - leave null if want to use temp collection
     * @param outputType
     *            set the type of job output
     * @param reduce
     *            reduce function in javascript code
     * @param query
     *            to match
     * @return
     * @throws MongoException
     * @dochub mapreduce
     */
    public MapReduceOutput mapReduce( String map , String reduce , String outputTarget , MapReduceCommand.OutputType outputType , DBObject query )
            throws MongoException{
        return mapReduce( new MapReduceCommand( this , map , reduce , outputTarget , outputType , query ) );
    }

    /**
     * performs a map reduce operation
     *
     * @param command
     *            object representing the parameters
     * @return
     * @throws MongoException
     */
    public MapReduceOutput mapReduce( MapReduceCommand command ) throws MongoException{
        DBObject cmd = command.toDBObject();
        // if type in inline, then query options like slaveOk is fine
        CommandResult res = null;
        if (command.getOutputType() == MapReduceCommand.OutputType.INLINE)
            res = _db.command( cmd, getOptions(), command.getReadPreference() != null ? command.getReadPreference() : getReadPreference() );
        else
            res = _db.command( cmd );
        res.throwOnError();
        return new MapReduceOutput( this , cmd, res );
    }

    /**
     * performs a map reduce operation
     *
     * @param command
     *            object representing the parameters
     * @return
     * @throws MongoException
     */
    public MapReduceOutput mapReduce( DBObject command ) throws MongoException{
        if ( command.get( "mapreduce" ) == null && command.get( "mapReduce" ) == null )
            throw new IllegalArgumentException( "need mapreduce arg" );
        CommandResult res = _db.command( command );
        res.throwOnError();
        return new MapReduceOutput( this , command, res );
    }

    /**
     *   Return a list of the indexes for this collection.  Each object
     *   in the list is the "info document" from MongoDB
     *
     *   @return list of index documents
     */
    public List<DBObject> getIndexInfo() {
        BasicDBObject cmd = new BasicDBObject();
        cmd.put("ns", getFullName());

        DBCursor cur = _db.getCollection("system.indexes").find(cmd);

        List<DBObject> list = new ArrayList<DBObject>();

        while(cur.hasNext()) {
            list.add(cur.next());
        }

        return list;
    }

    /**
     * Drops an index from this collection
     * @param keys keys of the index
     * @throws MongoException
     */
    public void dropIndex( DBObject keys )
        throws MongoException {
        dropIndexes( genIndexName( keys ) );
    }

    /**
     * Drops an index from this collection
     * @param name name of index to drop
     * @throws MongoException
     */
    public void dropIndex( String name )
        throws MongoException {
        dropIndexes( name );
    }

    /**
     * gets the collections statistics ("collstats" command)
     * @return
     */
    public CommandResult getStats() {
        return getDB().command(new BasicDBObject("collstats", getName()), getOptions());
    }

    /**
     * returns whether or not this is a capped collection
     * @return
     */
    public boolean isCapped() {
        CommandResult stats = getStats();
        Object capped = stats.get("capped");
        return(capped != null && (Integer)capped == 1);
    }

    // ------

    /**
     * Initializes a new collection. No operation is actually performed on the database.
     * @param base database in which to create the collection
     * @param name the name of the collection
     */
    protected DBCollection( DB base , String name ){
        _db = base;
        _name = name;
        _fullName = _db.getName() + "." + name;
        _options = new Bytes.OptionHolder( _db._options );
    }

    protected DBObject _checkObject( DBObject o , boolean canBeNull , boolean query ){
        if ( o == null ){
            if ( canBeNull )
                return null;
            throw new IllegalArgumentException( "can't be null" );
        }

        if ( o.isPartialObject() && ! query )
            throw new IllegalArgumentException( "can't save partial objects" );

        if ( ! query ){
            _checkKeys(o);
        }
        return o;
    }

    /**
     * Checks key strings for invalid characters.
     */
    private void _checkKeys( DBObject o ) {
        for ( String s : o.keySet() ){
            validateKey ( s );
            Object inner = o.get( s );
            if ( inner instanceof DBObject ) {
                _checkKeys( (DBObject)inner );
            } else if ( inner instanceof Map ) {
                _checkKeys( (Map<String, Object>)inner );
            }
        }
    }

    /**
     * Checks key strings for invalid characters.
     */
    private void _checkKeys( Map<String, Object> o ) {
        for ( String s : o.keySet() ){
            validateKey ( s );
            Object inner = o.get( s );
            if ( inner instanceof DBObject ) {
                _checkKeys( (DBObject)inner );
            } else if ( inner instanceof Map ) {
                _checkKeys( (Map<String, Object>)inner );
            }
        }
    }

    /**
     * Check for invalid key names
     * @param s the string field/key to check
     * @exception IllegalArgumentException if the key is not valid.
     */
    private void validateKey(String s ) {
        if ( s.contains( "." ) )
            throw new IllegalArgumentException( "fields stored in the db can't have . in them. (Bad Key: '" + s + "')" );
        if ( s.startsWith( "$" ) )
            throw new IllegalArgumentException( "fields stored in the db can't start with '$' (Bad Key: '" + s + "')" );
    }

    /**
     * Finds a collection that is prefixed with this collection's name.
     * A typical use of this might be
     * <blockquote><pre>
     *    DBCollection users = mongo.getCollection( "wiki" ).getCollection( "users" );
     * </pre></blockquote>
     * Which is equivalent to
     * <pre><blockquote>
     *   DBCollection users = mongo.getCollection( "wiki.users" );
     * </pre></blockquote>
     * @param n the name of the collection to find
     * @return the matching collection
     */
    public DBCollection getCollection( String n ){
        return _db.getCollection( _name + "." + n );
    }

    /**
     * Returns the name of this collection.
     * @return  the name of this collection
     */
    public String getName(){
        return _name;
    }

    /**
     * Returns the full name of this collection, with the database name as a prefix.
     * @return  the name of this collection
     */
    public String getFullName(){
        return _fullName;
    }

    /**
     * Returns the database this collection is a member of.
     * @return this collection's database
     */
    public DB getDB(){
        return _db;
    }

    /**
     * Returns if this collection's database is read-only
     * @param strict if an exception should be thrown if the database is read-only
     * @return if this collection's database is read-only
     * @throws RuntimeException if the database is read-only and <code>strict</code> is set
     */
    protected boolean checkReadOnly( boolean strict ){
        if ( ! _db._readOnly )
            return false;

        if ( ! strict )
            return true;

        throw new IllegalStateException( "db is read only" );
    }

    @Override
    public int hashCode(){
        return _fullName.hashCode();
    }

    @Override
    public boolean equals( Object o ){
        return o == this;
    }

    @Override
    public String toString(){
        return _name;
    }

    /**
     * Sets a default class for objects in this collection; null resets the class to nothing.
     * @param c the class
     * @throws IllegalArgumentException if <code>c</code> is not a DBObject
     */
    public void setObjectClass( Class c ){
        if ( c == null ){
            // reset
            _wrapper = null;
            _objectClass = null;
            return;
        }

        if ( ! DBObject.class.isAssignableFrom( c ) )
            throw new IllegalArgumentException( c.getName() + " is not a DBObject" );
        _objectClass = c;
        if ( ReflectionDBObject.class.isAssignableFrom( c ) )
            _wrapper = ReflectionDBObject.getWrapper( c );
        else
            _wrapper = null;
    }

    /**
     * Gets the default class for objects in the collection
     * @return the class
     */
    public Class getObjectClass(){
        return _objectClass;
    }

    /**
     * sets the internal class
     * @param path
     * @param c
     */
    public void setInternalClass( String path , Class c ){
        _internalClass.put( path , c );
    }

    /**
     * gets the internal class
     * @param path
     * @return
     */
    protected Class getInternalClass( String path ){
        Class c = _internalClass.get( path );
        if ( c != null )
            return c;

        if ( _wrapper == null )
            return null;
        return _wrapper.getInternalClass( path );
    }

    /**
     * Set the write concern for this collection. Will be used for
     * writes to this collection. Overrides any setting of write
     * concern at the DB level. See the documentation for
     * {@link WriteConcern} for more information.
     *
     * @param concern write concern to use
     */
    public void setWriteConcern( WriteConcern concern ){
        _concern = concern;
    }

    /**
     * Get the write concern for this collection.
     * @return
     */
    public WriteConcern getWriteConcern(){
        if ( _concern != null )
            return _concern;
        return _db.getWriteConcern();
    }

    /**
     * Sets the read preference for this collection. Will be used as default
     * for reads from this collection; overrides DB & Connection level settings.
     * See the * documentation for {@link ReadPreference} for more information.
     *
     * @param preference Read Preference to use
     */
    public void setReadPreference( ReadPreference preference ){
        _readPref = preference;
    }

    /**
     * Gets the read preference
     * @return
     */
    public ReadPreference getReadPreference(){
        if ( _readPref != null )
            return _readPref;
        return _db.getReadPreference();
    }
    /**
     * makes this query ok to run on a slave node
     *
     * @deprecated Replaced with ReadPreference.SECONDARY
     * @see com.mongodb.ReadPreference.SECONDARY
     */
    @Deprecated
    public void slaveOk(){
        addOption( Bytes.QUERYOPTION_SLAVEOK );
    }

    /**
     * adds a default query option
     * @param option
     */
    public void addOption( int option ){
        _options.add(option);
    }

    /**
     * sets the default query options
     * @param options
     */
    public void setOptions( int options ){
        _options.set(options);
    }

    /**
     * resets the default query options
     */
    public void resetOptions(){
        _options.reset();
    }

    /**
     * gets the default query options
     * @return
     */
    public int getOptions(){
        return _options.get();
    }

    /**
     * Set a customer decoder factory for this collection.  Set to null to use the default from MongoOptions.
     * @param fact  the factory to set.
     */
    public synchronized void setDBDecoderFactory(DBDecoderFactory fact) {
        _decoderFactory = fact;
    }

    /**
     * Get the decoder factory for this collection.  A null return value means that the default from MongoOptions
     * is being used.
     * @return  the factory
     */
    public synchronized DBDecoderFactory getDBDecoderFactory() {
        return _decoderFactory;
    }

    /**
     * Set a customer encoder factory for this collection.  Set to null to use the default from MongoOptions.
     * @param fact  the factory to set.
     */
    public synchronized void setDBEncoderFactory(DBEncoderFactory fact) {
        _encoderFactory = fact;
    }

    /**
     * Get the encoder factory for this collection.  A null return value means that the default from MongoOptions
     * is being used.
     * @return  the factory
     */
    public synchronized DBEncoderFactory getDBEncoderFactory() {
        return _encoderFactory;
    }

    final DB _db;

    final protected String _name;
    final protected String _fullName;

    protected List<DBObject> _hintFields;
    private WriteConcern _concern = null;
    private ReadPreference _readPref = null;
    private DBDecoderFactory _decoderFactory;
    private DBEncoderFactory _encoderFactory;
    final Bytes.OptionHolder _options;

    protected Class _objectClass = null;
    private Map<String,Class> _internalClass = Collections.synchronizedMap( new HashMap<String,Class>() );
    private ReflectionDBObject.JavaWrapper _wrapper = null;

    final private Set<String> _createdIndexes = new HashSet<String>();
}