public abstract class

DB

extends Object
// DB.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;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import com.mongodb.DBApiLayer.Result;
import com.mongodb.util.Util;

/**
 * an abstract class that represents a logical database on a server
 * @dochub databases
 */
public abstract class DB {

    /**
     * @param mongo the mongo instance
     * @param name the database name
     */
    public DB( Mongo mongo , String name ){
        _mongo = mongo;
    	_name = name;
        _options = new Bytes.OptionHolder( _mongo._netOptions );
    }

    /**
     * starts a new "consistent request".
     * Following this call and until requestDone() is called, all db operations should use the same underlying connection.
     * This is useful to ensure that operations happen in a certain order with predictable results.
     */
    public abstract void requestStart();

    /**
     * ends the current "consistent request"
     */
    public abstract void requestDone();

    /**
     * ensure that a connection is assigned to the current "consistent request" (from primary pool, if connected to a replica set)
     */
    public abstract void requestEnsureConnection();

    /**
     * Returns the collection represented by the string <dbName>.<collectionName>.
     * @param name the name of the collection
     * @return the collection
     */
    protected abstract DBCollection doGetCollection( String name );

    /**
     * Gets a collection with a given name.
     * If the collection does not exist, a new collection is created.
     * @param name the name of the collection to return
     * @return the collection
     */
    public DBCollection getCollection( String name ){
        DBCollection c = doGetCollection( name );
        return c;
    }

    /**
     * Creates a collection with a given name and options.
     * If the collection does not exist, a new collection is created.
     * Note that if the options parameter is null, the creation will be deferred to when the collection is written to.
     * Possible options:
     * <dl>
     * <dt>capped</dt><dd><i>boolean</i>: if the collection is capped</dd>
     * <dt>size</dt><dd><i>int</i>: collection size (in bytes)</dd>
     * <dt>max</dt><dd><i>int</i>: max number of documents</dd>
     * </dl>
     * @param name the name of the collection to return
     * @param options options
     * @return the collection
     */
    public DBCollection createCollection( String name, DBObject options ){
        if ( options != null ){
            DBObject createCmd = new BasicDBObject("create", name);
            createCmd.putAll(options);
            CommandResult result = command(createCmd);
            result.throwOnError();
        }
        return getCollection(name);
    }


    /**
     * Returns a collection matching a given string.
     * @param s the name of the collection
     * @return the collection
     */
    public DBCollection getCollectionFromString( String s ){
        DBCollection foo = null;

        int idx = s.indexOf( "." );
        while ( idx >= 0 ){
            String b = s.substring( 0 , idx );
            s = s.substring( idx + 1 );
            if ( foo == null )
                foo = getCollection( b );
            else
                foo = foo.getCollection( b );
            idx = s.indexOf( "." );
        }

        if ( foo != null )
            return foo.getCollection( s );
        return getCollection( s );
    }

    /**
     * Executes a database command.
     * This method calls {@link DB#command(com.mongodb.DBObject, int) } with 0 as query option.
     * @see <a href="http://mongodb.onconfluence.com/display/DOCS/List+of+Database+Commands">List of Commands</a>
     * @param cmd dbobject representing the command to execute
     * @return result of command from the database
     * @throws MongoException
     * @dochub commands
     */
    public CommandResult command( DBObject cmd ) throws MongoException{
        return command( cmd, 0 );
    }

    public CommandResult command( DBObject cmd, DBEncoder encoder ) throws MongoException{
        return command( cmd, 0, encoder );
    }

    public CommandResult command( DBObject cmd , int options, DBEncoder encoder )
            throws MongoException {
        return command(cmd, options, getReadPreference(), encoder);
    }

    public CommandResult command( DBObject cmd , int options, ReadPreference readPrefs )
            throws MongoException {
        return command(cmd, options, readPrefs, DefaultDBEncoder.FACTORY.create());
    }

    /**
     * Executes a database command.
     * @see <a href="http://mongodb.onconfluence.com/display/DOCS/List+of+Database+Commands">List of Commands</a>
     * @param cmd dbobject representing the command to execute
     * @param options query options to use
     * @param readPrefs ReadPreferences for this command (nodes selection is the biggest part of this)
     * @return result of command from the database
     * @dochub commands
     * @throws MongoException
     */
    public CommandResult command( DBObject cmd , int options, ReadPreference readPrefs, DBEncoder encoder )
        throws MongoException {

        Iterator<DBObject> i =
                getCollection("$cmd").__find(cmd, new BasicDBObject(), 0, -1, 0, options, readPrefs ,
                        DefaultDBDecoder.FACTORY.create(), encoder);
        if ( i == null || ! i.hasNext() )
            return null;

        DBObject res = i.next();
        ServerAddress sa = (i instanceof Result) ? ((Result) i).getServerAddress() : null;
        CommandResult cr = new CommandResult(cmd, sa);
        cr.putAll( res );
        return cr;
    }

    /**
     * Executes a database command.
     * @see <a href="http://mongodb.onconfluence.com/display/DOCS/List+of+Database+Commands">List of Commands</a>
     * @param cmd dbobject representing the command to execute
     * @param options query options to use
     * @return result of command from the database
     * @dochub commands
     * @throws MongoException
     */
    public CommandResult command( DBObject cmd , int options )
        throws MongoException {
	return command(cmd, options, getReadPreference());
    }
    /**
     * Executes a database command.
     * This method constructs a simple dbobject and calls {@link DB#command(com.mongodb.DBObject) }
     * @see <a href="http://mongodb.onconfluence.com/display/DOCS/List+of+Database+Commands">List of Commands</a>
     * @param cmd command to execute
     * @return result of command from the database
     * @throws MongoException
     */
    public CommandResult command( String cmd )
        throws MongoException {
        return command( new BasicDBObject( cmd , Boolean.TRUE ) );
    }

    /**
     * Executes a database command.
     * This method constructs a simple dbobject and calls {@link DB#command(com.mongodb.DBObject, int)  }
     * @see <a href="http://mongodb.onconfluence.com/display/DOCS/List+of+Database+Commands">List of Commands</a>
     * @param cmd command to execute
     * @param options query options to use
     * @return result of command from the database
     * @throws MongoException
     */
    public CommandResult command( String cmd, int options  )
        throws MongoException {
        return command( new BasicDBObject( cmd , Boolean.TRUE ), options );
    }

    /**
     * evaluates a function on the database.
     * This is useful if you need to touch a lot of data lightly, in which case network transfer could be a bottleneck.
     * @param code the function in javascript code
     * @param args arguments to be passed to the function
     * @return The command result
     * @throws MongoException
     */
    public CommandResult doEval( String code , Object ... args )
        throws MongoException {

        return command( BasicDBObjectBuilder.start()
                        .add( "$eval" , code )
                        .add( "args" , args )
                        .get() );
    }

    /**
     * calls {@link DB#doEval(java.lang.String, java.lang.Object[]) }.
     * If the command is successful, the "retval" field is extracted and returned.
     * Otherwise an exception is thrown.
     * @param code the function in javascript code
     * @param args arguments to be passed to the function
     * @return The object
     * @throws MongoException
     */
    public Object eval( String code , Object ... args )
        throws MongoException {

        CommandResult res = doEval( code , args );
        res.throwOnError();
        return res.get( "retval" );
    }

    /**
     * Returns the result of "dbstats" command
     * @return
     */
    public CommandResult getStats() {
        return command("dbstats");
    }

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

    /**
     * Makes this database read-only.
     * Important note: this is a convenience setting that is only known on the client side and not persisted.
     * @param b if the database should be read-only
     */
    public void setReadOnly( Boolean b ){
        _readOnly = b;
    }

    /**
     * Returns a set containing the names of all collections in this database.
     * @return the names of collections in this database
     * @throws MongoException
     */
    public Set<String> getCollectionNames()
        throws MongoException {

        DBCollection namespaces = getCollection("system.namespaces");
        if (namespaces == null)
            throw new RuntimeException("this is impossible");

        // TODO - Is ReadPreference OK for collection Names?
        Iterator<DBObject> i = namespaces.__find(new BasicDBObject(), null, 0, 0, 0, getOptions(), null, null);
        if (i == null)
            return new HashSet<String>();

        List<String> tables = new ArrayList<String>();

        for (; i.hasNext();) {
            DBObject o = i.next();
            if ( o.get( "name" ) == null ){
                throw new MongoException( "how is name null : " + o );
            }
            String n = o.get("name").toString();
            int idx = n.indexOf(".");

            String root = n.substring(0, idx);
            if (!root.equals(_name))
                continue;

            if (n.indexOf("$") >= 0)
                continue;

            String table = n.substring(idx + 1);

            tables.add(table);
        }

        Collections.sort(tables);

        return new LinkedHashSet<String>(tables);
    }

    /**
     * Checks to see if a collection by name %lt;name&gt; exists.
     * @param collectionName The collection to test for existence
     * @return false if no collection by that name exists, true if a match to an existing collection was found
     */
    public boolean collectionExists(String collectionName)
    {
        if (collectionName == null || "".equals(collectionName))
            return false;

        Set<String> collections = getCollectionNames();
        if (collections.isEmpty())
            return false;

        for (String collection : collections)
        {
            if (collectionName.equalsIgnoreCase(collection))
                return true;
        }

        return false;
    }


    /**
     * Returns the name of this database.
     * @return the name
     */
    @Override
    public String toString(){
        return _name;
    }

    /**
     * Gets the the error (if there is one) from the previous operation on this connection.
     * The result of this command will look like
     *
     * <pre>
     * { "err" :  errorMessage  , "ok" : 1.0 }
     * </pre>
     *
     * The value for errorMessage will be null if no error occurred, or a description otherwise.
     *
     * Important note: when calling this method directly, it is undefined which connection "getLastError" is called on.
     * You may need to explicitly use a "consistent Request", see {@link DB#requestStart()}
     * For most purposes it is better not to call this method directly but instead use {@link WriteConcern}
     *
     * @return DBObject with error and status information
     * @throws MongoException
     */
    public CommandResult getLastError()
        throws MongoException {
        return command(new BasicDBObject("getlasterror", 1));
    }

    /**
     * @see {@link DB#getLastError() }
     * @param concern the concern associated with "getLastError" call
     * @return
     * @throws MongoException
     */
    public CommandResult getLastError( com.mongodb.WriteConcern concern )
        throws MongoException {
        return command( concern.getCommand() );
    }

    /**
     * @see {@link DB#getLastError(com.mongodb.WriteConcern) }
     * @param w
     * @param wtimeout
     * @param fsync
     * @return The command result
     * @throws MongoException
     */
    public CommandResult getLastError( int w , int wtimeout , boolean fsync )
        throws MongoException {
        return command( (new com.mongodb.WriteConcern( w, wtimeout , fsync )).getCommand() );
    }


    /**
     * Sets the write concern for this database. It Will be used for
     * writes to any collection in this database. See the
     * documentation for {@link WriteConcern} for more information.
     * @param concern write concern to use
     */
    public void setWriteConcern( com.mongodb.WriteConcern concern ){
        if (concern == null) throw new IllegalArgumentException();
        _concern = concern;
    }

    /**
     * Gets the write concern for this database.
     * @return
     */
    public com.mongodb.WriteConcern getWriteConcern(){
        if ( _concern != null )
            return _concern;
        return _mongo.getWriteConcern();
    }

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

    /**
     * Gets the default read preference
     * @return
     */
    public ReadPreference getReadPreference(){
        if ( _readPref != null )
            return _readPref;
        return _mongo.getReadPreference();
    }

    /**
     * Drops this database. Removes all data on disk. Use with caution.
     * @throws MongoException
     */
    public void dropDatabase()
        throws MongoException {

        CommandResult res = command(new BasicDBObject("dropDatabase", 1));
        res.throwOnError();
        _mongo._dbs.remove(this.getName());
    }

    /**
     * Returns true if a user has been authenticated
     *
     * @return true if authenticated, false otherwise
     * @dochub authenticate
     */
    public boolean isAuthenticated() {
	return ( _username != null );
    }

    /**
     *  Authenticates to db with the given name and password
     *
     * @param username name of user for this database
     * @param passwd password of user for this database
     * @return true if authenticated, false otherwise
     * @throws MongoException
     * @dochub authenticate
     */
    public boolean authenticate(String username, char[] passwd )
        throws MongoException {

        if ( username == null || passwd == null )
            throw new NullPointerException( "username can't be null" );

        if ( _username != null )
	    throw new IllegalStateException( "can't call authenticate twice on the same DBObject" );

        String hash = _hash( username , passwd );
        CommandResult res = _doauth( username , hash.getBytes() );
        if ( !res.ok())
            return false;
        _username = username;
        _authhash = hash.getBytes();
        return true;
    }

    /**
     *  Authenticates to db with the given name and password
     *
     * @param username name of user for this database
     * @param passwd password of user for this database
     * @return the CommandResult from authenticate command
     * @throws MongoException if authentication failed due to invalid user/pass, or other exceptions like I/O
     * @dochub authenticate
     */
    public CommandResult authenticateCommand(String username, char[] passwd )
        throws MongoException {

        if ( username == null || passwd == null )
            throw new NullPointerException( "username can't be null" );

        if ( _username != null )
	    throw new IllegalStateException( "can't call authenticate twice on the same DBObject" );

        String hash = _hash( username , passwd );
        CommandResult res = _doauth( username , hash.getBytes() );
        res.throwOnError();
        _username = username;
        _authhash = hash.getBytes();
        return res;
    }

    /*
    boolean reauth(){
        if ( _username == null || _authhash == null )
            throw new IllegalStateException( "no auth info!" );
        return _doauth( _username , _authhash );
    }
    */

    DBObject _authCommand( String nonce ){
        if ( _username == null || _authhash == null )
            throw new IllegalStateException( "no auth info!" );

        return _authCommand( nonce , _username , _authhash );
    }

    static DBObject _authCommand( String nonce , String username , byte[] hash ){
        String key = nonce + username + new String( hash );

        BasicDBObject cmd = new BasicDBObject();

        cmd.put("authenticate", 1);
        cmd.put("user", username);
        cmd.put("nonce", nonce);
        cmd.put("key", Util.hexMD5(key.getBytes()));

        return cmd;
    }

    private CommandResult _doauth( String username , byte[] hash ){
        CommandResult res = command(new BasicDBObject("getnonce", 1));
        res.throwOnError();

        DBObject cmd = _authCommand( res.getString( "nonce" ) , username , hash );
        return command(cmd);
    }

    /**
     * Adds a new user for this db
     * @param username
     * @param passwd
     */
    public WriteResult addUser( String username , char[] passwd ){
        return addUser(username, passwd, false);
    }

    /**
     * Adds a new user for this db
     * @param username
     * @param passwd
     * @param readOnly if true, user will only be able to read
     */
    public WriteResult addUser( String username , char[] passwd, boolean readOnly ){
        DBCollection c = getCollection( "system.users" );
        DBObject o = c.findOne( new BasicDBObject( "user" , username ) );
        if ( o == null )
            o = new BasicDBObject( "user" , username );
        o.put( "pwd" , _hash( username , passwd ) );
        o.put( "readOnly" , readOnly );
        return c.save( o );
    }

    /**
     * Removes a user for this db
     * @param username
     */
    public WriteResult removeUser( String username ){
        DBCollection c = getCollection( "system.users" );
        return c.remove(new BasicDBObject( "user" , username ));
    }

    String _hash( String username , char[] passwd ){
        ByteArrayOutputStream bout = new ByteArrayOutputStream( username.length() + 20 + passwd.length );
        try {
            bout.write( username.getBytes() );
            bout.write( ":mongo:".getBytes() );
            for ( int i=0; i<passwd.length; i++ ){
                if ( passwd[i] >= 128 )
                    throw new IllegalArgumentException( "can't handle non-ascii passwords yet" );
                bout.write( (byte)passwd[i] );
            }
        }
        catch ( IOException ioe ){
            throw new RuntimeException( "impossible" , ioe );
        }
        return Util.hexMD5( bout.toByteArray() );
    }

    /**
     *  Returns the last error that occurred since start of database or a call to <code>resetError()</code>
     *
     *  The return object will look like
     *
     *  <pre>
     * { err : errorMessage, nPrev : countOpsBack, ok : 1 }
     *  </pre>
     *
     * The value for errorMessage will be null of no error has occurred, otherwise the error message.
     * The value of countOpsBack will be the number of operations since the error occurred.
     *
     * Care must be taken to ensure that calls to getPreviousError go to the same connection as that
     * of the previous operation.
     * See {@link DB#requestStart()} for more information.
     *
     * @return DBObject with error and status information
     * @throws MongoException
     */
    public CommandResult getPreviousError()
        throws MongoException {
        return command(new BasicDBObject("getpreverror", 1));
    }

    /**
     * Resets the error memory for this database.
     * Used to clear all errors such that {@link DB#getPreviousError()} will return no error.
     * @throws MongoException
     */
    public void resetError()
        throws MongoException {
        command(new BasicDBObject("reseterror", 1));
    }

    /**
     * For testing purposes only - this method forces an error to help test error handling
     * @throws MongoException
     */
    public void forceError()
        throws MongoException {
        command(new BasicDBObject("forceerror", 1));
    }

    /**
     * Gets the Mongo instance
     * @return
     */
    public Mongo getMongo(){
        return _mongo;
    }

    /**
     * Gets another database on same server
     * @param name name of the database
     * @return
     */
    public DB getSisterDB( String name ){
        return _mongo.getDB( name );
    }

    /**
     * Makes it possible to execute "read" queries on a slave node
     *
     * @deprecated Replaced with ReadPreference.SECONDARY
     * @see com.mongodb.ReadPreference.SECONDARY
     */
    @Deprecated
    public void slaveOk(){
        addOption( Bytes.QUERYOPTION_SLAVEOK );
    }

    /**
     * Adds the give option
     * @param option
     */
    public void addOption( int option ){
        _options.add( option );
    }

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

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

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

    public abstract void cleanCursors( boolean force ) throws MongoException;


    final Mongo _mongo;
    final String _name;

    protected boolean _readOnly = false;
    private com.mongodb.WriteConcern _concern;
    private com.mongodb.ReadPreference _readPref;
    final Bytes.OptionHolder _options;

    String _username;
    byte[] _authhash = null;

}