public static class

Mongo.Holder

extends Object
// Mongo.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.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.bson.io.PoolOutputBuffer;

/**
 * A database connection with internal pooling.
 * For most application, you should have 1 Mongo instance for the entire JVM.
 *
 * The following are equivalent, and all connect to the
 * local database running on the default port:
 *
 * <blockquote><pre>
 * Mongo mongo1 = new Mongo( "127.0.0.1" );
 * Mongo mongo2 = new Mongo( "127.0.0.1", 27017 );
 * Mongo mongo3 = new Mongo( new DBAddress( "127.0.0.1", 27017, "test" ) );
 * Mongo mongo4 = new Mongo( new ServerAddress( "127.0.0.1") );
 * </pre></blockquote>
 *
 * Mongo instances have connection pooling built in - see the requestStart
 * and requestDone methods for more information.
 * http://www.mongodb.org/display/DOCS/Java+Driver+Concurrency
 *
 * <h3>Connecting to a Replica Set</h3>
 * <p>
 * You can connect to a
 * <a href="http://www.mongodb.org/display/DOCS/Replica+Sets">replica set</a>
 * using the Java driver by passing several a list if ServerAddress to the
 * Mongo constructor.
 * For example:
 * </p>
 * <blockquote><pre>
 * List<ServerAddress> addrs = new ArrayList<ServerAddress>();
 * addrs.add( new ServerAddress( "127.0.0.1" , 27017 ) );
 * addrs.add( new ServerAddress( "127.0.0.1" , 27018 ) );
 * addrs.add( new ServerAddress( "127.0.0.1" , 27019 ) );
 *
 * Mongo mongo = new Mongo( addrs );
 * </pre></blockquote>
 *
 * <p>
 * By default, all read and write operations will be made on the master.
 * But it's possible to read from the slave(s) by using slaveOk:
 * </p>
 * <blockquote><pre>
 * mongo.slaveOk();
 * </pre></blockquote>
 */
public class Mongo {

    // Make sure you don't change the format of these two static variables. A preprocessing regexp
    // is applied and updates the version based on configuration in build.properties.

    /**
     * @deprecated Replaced by <code>Mongo.getMajorVersion()</code>
     */
    @Deprecated
    public static final int MAJOR_VERSION = 2;

    /**
     * @deprecated Replaced by <code>Mongo.getMinorVersion()</code>
     */
    @Deprecated
    public static final int MINOR_VERSION = 8;

    static int cleanerIntervalMS;
    static {
        cleanerIntervalMS = Integer.parseInt(System.getProperty("com.mongodb.cleanerIntervalMS", "1000"));
    }

    /**
     * Gets the major version of this library
     * @return the major version, e.g. 2
     */
    public static int getMajorVersion() {
        return MAJOR_VERSION;
    }

    /**
     * Gets the minor version of this library
     * @return the minor version, e.g. 8
     */
    public static int getMinorVersion() {
        return MINOR_VERSION;
    }

    /**
     * returns a database object
     * @param addr the database address
     * @return
     */
    public static DB connect( DBAddress addr ){
        return new Mongo( addr ).getDB( addr.getDBName() );
    }

    /**
     * Creates a Mongo instance based on a (single) mongodb node (localhost, default port)
     * @throws UnknownHostException
     * @throws MongoException
     */
    public Mongo()
        throws UnknownHostException , MongoException {
        this( new ServerAddress() );
    }

    /**
     * Creates a Mongo instance based on a (single) mongodb node (default port)
     * @param host server to connect to
     * @throws UnknownHostException if the database host cannot be resolved
     * @throws MongoException
     */
    public Mongo( String host )
        throws UnknownHostException , MongoException {
        this( new ServerAddress( host ) );
    }

    /**
     * Creates a Mongo instance based on a (single) mongodb node (default port)
     * @param host server to connect to
     * @param options default query options
     * @throws UnknownHostException if the database host cannot be resolved
     * @throws MongoException
     */
    public Mongo( String host , MongoOptions options )
        throws UnknownHostException , MongoException {
        this( new ServerAddress( host ) , options );
    }

    /**
     * Creates a Mongo instance based on a (single) mongodb node
     * @param host the database's host address
     * @param port the port on which the database is running
     * @throws UnknownHostException if the database host cannot be resolved
     * @throws MongoException
     */
    public Mongo( String host , int port )
        throws UnknownHostException , MongoException {
        this( new ServerAddress( host , port ) );
    }

    /**
     * Creates a Mongo instance based on a (single) mongodb node
     * @see com.mongodb.ServerAddress
     * @param addr the database address
     * @throws MongoException
     */
    public Mongo( ServerAddress addr )
        throws MongoException {
        this( addr , new MongoOptions() );
    }

    /**
     * Creates a Mongo instance based on a (single) mongo node using a given ServerAddress
     * @see com.mongodb.ServerAddress
     * @param addr the database address
     * @param options default query options
     * @throws MongoException
     */
    public Mongo( ServerAddress addr , MongoOptions options )
        throws MongoException {
        _addr = addr;
        _addrs = null;
        _options = options;
        _applyMongoOptions();
        _connector = new DBTCPConnector( this , _addr );
        _connector.start();
        _cleaner = new DBCleanerThread();
        _cleaner.start();
    }

    /**
     * <p>Creates a Mongo in paired mode. <br/> This will also work for
     * a replica set and will find all members (the master will be used by
     * default).</p>
     *
     * @see com.mongodb.ServerAddress
     * @param left left side of the pair
     * @param right right side of the pair
     * @throws MongoException
     */
    @Deprecated
    public Mongo( ServerAddress left , ServerAddress right )
        throws MongoException {
        this( left , right , new MongoOptions() );
    }

    /**
     * <p>Creates a Mongo connection in paired mode. <br/> This will also work for
     * a replica set and will find all members (the master will be used by
     * default).</p>
     *
     * @see com.mongodb.ServerAddress
     * @param left left side of the pair
     * @param right right side of the pair
     * @param options
     * @throws MongoException
     */
    @Deprecated
    public Mongo( ServerAddress left , ServerAddress right , MongoOptions options )
        throws MongoException {
        _addr = null;
        _addrs = Arrays.asList( left , right );
        _options = options;
        _applyMongoOptions();
        _connector = new DBTCPConnector( this , _addrs );
        _connector.start();

        _cleaner = new DBCleanerThread();
        _cleaner.start();
    }

    /**
     * <p>Creates a Mongo based on a replica set, or pair.
     * It will find all members (the master will be used by default). If you pass in a single server in the list,
     * the driver will still function as if it is a replica set. If you have a standalone server,
     * use the Mongo(ServerAddress) constructor.</p>
     * @see com.mongodb.ServerAddress
     * @param replicaSetSeeds Put as many servers as you can in the list and
     * the system will figure out the rest.
     * @throws MongoException
     */
    public Mongo( List<ServerAddress> replicaSetSeeds )
        throws MongoException {
        this( replicaSetSeeds , new MongoOptions() );
    }

    /**
     * <p>Creates a Mongo based on a replica set, or pair.
     * It will find all members (the master will be used by default).</p>
     * @see com.mongodb.ServerAddress
     * @param replicaSetSeeds put as many servers as you can in the list.
     *                       the system will figure the rest out
     * @param options default query options
     * @throws MongoException
     */
    public Mongo( List<ServerAddress> replicaSetSeeds , MongoOptions options )
        throws MongoException {

        _addr = null;
        _addrs = replicaSetSeeds;
        _options = options;
        _applyMongoOptions();
        _connector = new DBTCPConnector( this , _addrs );
        _connector.start();

        _cleaner = new DBCleanerThread();
        _cleaner.start();
    }

    /**
     * Creates a Mongo described by a URI.
     * If only one address is used it will only connect to that node, otherwise it will discover all nodes.
     * @param uri
     * @see MongoURI
     * <p>examples:
     *   <li>mongodb://127.0.0.1</li>
     *   <li>mongodb://fred:foobar@127.0.0.1/</li>
     *  </p>
     *  @throws MongoException
     * @throws UnknownHostException
     * @dochub connections
     */

    public Mongo( MongoURI uri )
        throws MongoException , UnknownHostException {

        _options = uri.getOptions();
        _applyMongoOptions();

        if ( uri.getHosts().size() == 1 ){
            _addr = new ServerAddress( uri.getHosts().get(0) );
            _addrs = null;
            _connector = new DBTCPConnector( this , _addr );
        }
        else {
            List<ServerAddress> replicaSetSeeds = new ArrayList<ServerAddress>( uri.getHosts().size() );
            for ( String host : uri.getHosts() )
                replicaSetSeeds.add( new ServerAddress( host ) );
            _addr = null;
            _addrs = replicaSetSeeds;
            _connector = new DBTCPConnector( this , replicaSetSeeds );
        }

        _connector.start();
        _cleaner = new DBCleanerThread();
        _cleaner.start();
    }

    /**
     * gets a database object
     * @param dbname the database name
     * @return
     */
    public DB getDB( String dbname ){

        DB db = _dbs.get( dbname );
        if ( db != null )
            return db;

        db = new DBApiLayer( this , dbname , _connector );
        DB temp = _dbs.putIfAbsent( dbname , db );
        if ( temp != null )
            return temp;
        return db;
    }

    /**
     * gets a collection of DBs used by the driver since this Mongo instance was created.
     * This may include DBs that exist in the client but not yet on the server.
     * @return
     */
    public Collection<DB> getUsedDatabases(){
        return _dbs.values();
    }

    /**
     * gets a list of all database names present on the server
     * @return
     * @throws MongoException
     */
    public List<String> getDatabaseNames()
        throws MongoException {

        BasicDBObject cmd = new BasicDBObject();
        cmd.put("listDatabases", 1);


        CommandResult res = getDB( "admin" ).command(cmd, getOptions());
        res.throwOnError();

        List l = (List)res.get("databases");

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

        for (Object o : l) {
            list.add(((BasicDBObject)o).getString("name"));
        }
        return list;
    }


    /**
     * Drops the database if it exists.
     * @param dbName name of database to drop
     * @throws MongoException
     */
    public void dropDatabase(String dbName)
        throws MongoException {

        getDB( dbName ).dropDatabase();
    }

    /**
     * gets this driver version
     * @return
     */
    public String getVersion(){
        return MAJOR_VERSION + "." + MINOR_VERSION;
    }

    /**
     * returns a string representing the hosts used in this Mongo instance
     * @return
     */
    public String debugString(){
        return _connector.debugString();
    }

    /**
     * Gets the current master's hostname
     * @return
     */
    public String getConnectPoint(){
        return _connector.getConnectPoint();
    }

    /**
     * Gets the underlying TCP connector
     * @return
     */
    public DBTCPConnector getConnector() {
        return _connector;
    }

    /**
     * Gets the replica set status object
     * @return
     */
    public ReplicaSetStatus getReplicaSetStatus() {
        return _connector.getReplicaSetStatus();
    }

    /**
     * Gets the address of the current master
     * @return the address
     */
    public ServerAddress getAddress(){
        return _connector.getAddress();
    }

    /**
     * Gets a list of all server addresses used when this Mongo was created
     * @return
     */
    public List<ServerAddress> getAllAddress() {
        List<ServerAddress> result = _connector.getAllAddress();
        if (result == null) {
            return Arrays.asList(getAddress());
        }
        return result;
    }

    /**
     * Gets the list of server addresses currently seen by the connector.
     * This includes addresses auto-discovered from a replica set.
     * @return
     */
    public List<ServerAddress> getServerAddressList() {
        return _connector.getServerAddressList();
    }

    /**
     * closes the underlying connector, which in turn closes all open connections.
     * Once called, this Mongo instance can no longer be used.
     */
    public void close(){

        try {
            _connector.close();
        } catch (final Throwable t) { /* nada */ }

        _cleaner.interrupt();

        try {
            _cleaner.join();
        } catch (InterruptedException e) {
            //end early
        }
    }

    /**
     * Sets the write concern for this database. Will be used as default for
     * writes to any collection in any database. See the
     * documentation for {@link WriteConcern} for more information.
     *
     * @param concern write concern to use
     */
    public void setWriteConcern( WriteConcern concern ){
        _concern = concern;
    }

    /**
     * Gets the default write concern
     * @return
     */
    public WriteConcern getWriteConcern(){
        return _concern;
    }

    /**
     * Sets the read preference for this database. Will be used as default for
     * reads from any collection in any 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(){
        return _readPref;
    }

    /**
     * makes it possible to run read queries on slave nodes
     *
     * @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 ){
        _netOptions.add( option );
    }

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

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

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

    /**
     * Helper method for setting up MongoOptions at instantiation
     * so that any options which affect this connection can be set.
     */
    @SuppressWarnings("deprecation")
    void _applyMongoOptions() {
        if (_options.slaveOk) slaveOk();
        setWriteConcern( _options.getWriteConcern() );
    }

    /**
     * Returns the mongo options.
     */
    public MongoOptions getMongoOptions() {
        return _options;
    }

    /**
     * Gets the maximum size for a BSON object supported by the current master server.
     * Note that this value may change over time depending on which server is master.
     * If the size is not known yet, a request may be sent to the master server
     * @return the maximum size
     */
    public int getMaxBsonObjectSize() {
        int maxsize = _connector.getMaxBsonObjectSize();
        if (maxsize == 0)
            maxsize = _connector.fetchMaxBsonObjectSize();
        return maxsize > 0 ? maxsize : Bytes.MAX_OBJECT_SIZE;
    }

    final ServerAddress _addr;
    final List<ServerAddress> _addrs;
    final MongoOptions _options;
    final DBTCPConnector _connector;
    final ConcurrentMap<String,DB> _dbs = new ConcurrentHashMap<String,DB>();
    private WriteConcern _concern = WriteConcern.NORMAL;
    private ReadPreference _readPref = ReadPreference.PRIMARY;
    final Bytes.OptionHolder _netOptions = new Bytes.OptionHolder( null );
    final DBCleanerThread _cleaner;

    org.bson.util.SimplePool<PoolOutputBuffer> _bufferPool =
        new org.bson.util.SimplePool<PoolOutputBuffer>( 1000 ){

        protected PoolOutputBuffer createNew(){
            return new PoolOutputBuffer();
        }

    };

    /**
     * Forces the master server to fsync the RAM data to disk
     * This is done automatically by the server at intervals, but can be forced for better reliability. 
     * @param async if true, the fsync will be done asynchronously on the server.
     * @return 
     */
    public CommandResult fsync(boolean async) {
        DBObject cmd = new BasicDBObject("fsync", 1);
        if (async) {
            cmd.put("async", 1);
        }
        return getDB("admin").command(cmd);
    }

    /**
     * Forces the master server to fsync the RAM data to disk, then lock all writes.
     * The database will be read-only after this command returns.
     * @return 
     */
    public CommandResult fsyncAndLock() {
        DBObject cmd = new BasicDBObject("fsync", 1);
        cmd.put("lock", 1);
        return getDB("admin").command(cmd);
    }

    /**
     * Unlocks the database, allowing the write operations to go through.
     * This command may be asynchronous on the server, which means there may be a small delay before the database becomes writable.
     * @return 
     */
    public DBObject unlock() {
        DB db = getDB("admin");
        DBCollection col = db.getCollection("$cmd.sys.unlock");
        return col.findOne();
    }

    /**
     * Returns true if the database is locked (read-only), false otherwise.
     * @return 
     */
    public boolean isLocked() {
        DB db = getDB("admin");
        DBCollection col = db.getCollection("$cmd.sys.inprog");
        BasicDBObject res = (BasicDBObject) col.findOne();
        if (res.containsField("fsyncLock")) {
            return res.getInt("fsyncLock") == 1;
        }
        return false;
    }

    // -------


    /**
     * Mongo.Holder can be used as a static place to hold several instances of Mongo.
     * Security is not enforced at this level, and needs to be done on the application side.
     */
    public static class Holder {

        /**
         * Attempts to find an existing Mongo instance matching that URI in the holder, and returns it if exists.
         * Otherwise creates a new Mongo instance based on this URI and adds it to the holder.
         * @param uri the Mongo URI
         * @return
         * @throws MongoException
         * @throws UnknownHostException
         */
        public Mongo connect( MongoURI uri )
            throws MongoException , UnknownHostException {

            String key = _toKey( uri );

            Mongo m = _mongos.get(key);
            if ( m != null )
                return m;

            m = new Mongo( uri );

            Mongo temp = _mongos.putIfAbsent( key , m );
            if ( temp == null ){
                // ours got in
                return m;
            }

            // there was a race and we lost
            // close ours and return the other one
            m.close();
            return temp;
        }

        String _toKey( MongoURI uri ){
            StringBuilder buf = new StringBuilder();
            for ( String h : uri.getHosts() )
                buf.append( h ).append( "," );
            buf.append( uri.getOptions() );
            buf.append( uri.getUsername() );
            return buf.toString();
        }
        
        public static Holder singleton() { return _default; }

        private static Holder _default = new Holder();
        private final ConcurrentMap<String,Mongo> _mongos = new ConcurrentHashMap<String,Mongo>();

    }

    class DBCleanerThread extends Thread {

        DBCleanerThread() {
            setDaemon(true);
            setName("MongoCleaner" + hashCode());
        }

        public void run() {
            while (_connector.isOpen()) {
                try {
                    try {
                        Thread.sleep(cleanerIntervalMS);
                    } catch (InterruptedException e) {
                        //caused by the Mongo instance being closed -- proceed with cleanup
                    }
                    for (DB db : _dbs.values()) {
                        db.cleanCursors(true);
                    }
                } catch (Throwable t) {
                    // thread must never die
                }
            }
        }
    }

    @Override
    public String toString() {
        StringBuilder str = new StringBuilder("Mongo: ");
        List<ServerAddress> list = getServerAddressList();
        if (list == null || list.size() == 0)
            str.append("null");
        else {
            for ( ServerAddress addr : list )
                str.append( addr.toString() ).append( ',' );
            str.deleteCharAt( str.length() - 1 );
        }
        return str.toString();
    }
}