// 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> 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; }