From affbe71f9322cba66bcddada2d596c8b8b81ad24 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 4 Jan 2011 16:56:35 +0100 Subject: [PATCH] DATADOC-4 - Improved MongoTemplate exception handling - moved exception translation from MongoDbUtils to MongoExceptionTranslator - don't let MongoFactoryBean implement PersistenceExceptionTranslator - heavily refactored MongoTemplate to let higher level methods use callbacks to centralize exception handling in the callback executing methods - changed return type of MongoOperations.getCollectionNames to Set as returning a List implies same order, that we can't guarantee actually - polished JavaDoc of MongoOperations - added assertions to methods - changed methods taking a List to take a List instead - added abstract unit test for MongoOperations to define tests regarding expected exception behavior - let MongoTemplateUnitTests extend MongoOperationsUnitTests --- .../data/document/mongodb/MongoDbUtils.java | 37 -- .../mongodb/MongoExceptionTranslator.java | 47 +- .../document/mongodb/MongoFactoryBean.java | 11 +- .../document/mongodb/MongoOperations.java | 17 +- .../data/document/mongodb/MongoTemplate.java | 458 +++++++++++------- .../mongodb/MongoOperationsUnitTests.java | 344 +++++++++++++ .../mongodb/MongoTemplateUnitTests.java | 26 +- .../data/document/mongodb/Person.java | 8 + 8 files changed, 704 insertions(+), 244 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoOperationsUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoDbUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoDbUtils.java index 6cb6f766f..a1c9201a5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoDbUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoDbUtils.java @@ -18,18 +18,11 @@ package org.springframework.data.document.mongodb; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.dao.DataAccessException; -import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.data.document.UncategorizedDocumentStoreException; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import com.mongodb.DB; import com.mongodb.Mongo; -import com.mongodb.MongoException; -import com.mongodb.MongoException.DuplicateKey; -import com.mongodb.MongoException.Network; /** * Helper class featuring helper methods for internal MongoDb classes. @@ -52,36 +45,6 @@ abstract class MongoDbUtils { private MongoDbUtils() { } - - /** - * Convert the given runtime exception to an appropriate exception from the - * org.springframework.dao hierarchy. - * Return null if no translation is appropriate: any other exception may - * have resulted from user code, and should not be translated. - * @param ex runtime exception that occurred - * @return the corresponding DataAccessException instance, - * or null if the exception should not be translated - */ - public static DataAccessException translateMongoExceptionIfPossible(RuntimeException ex) { - - // Check for well-known MongoException subclasses. - - // All other MongoExceptions - if(ex instanceof DuplicateKey) { - return new DataIntegrityViolationException(ex.getMessage(),ex); - } - if(ex instanceof Network) { - return new DataAccessResourceFailureException(ex.getMessage(), ex); - } - if (ex instanceof MongoException) { - return new UncategorizedDocumentStoreException(ex.getMessage(), ex); - } - - // If we get here, we have an exception that resulted from user code, - // rather than the persistence provider, so we return null to indicate - // that translation should not occur. - return null; - } /** * Obtains a {@link DB} connection for the given {@link Mongo} instance and database name diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoExceptionTranslator.java index 013876701..f92ac499f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoExceptionTranslator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoExceptionTranslator.java @@ -16,24 +16,51 @@ package org.springframework.data.document.mongodb; import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.data.document.UncategorizedDocumentStoreException; +import com.mongodb.MongoException; +import com.mongodb.MongoException.DuplicateKey; +import com.mongodb.MongoException.Network; /** - * Simple {@link PersistenceExceptionTranslator} for Mongo. + * Simple {@link PersistenceExceptionTranslator} for Mongo. Convert the given runtime exception to an appropriate + * exception from the {@code org.springframework.dao} hierarchy. Return {@literal null} if no translation is + * appropriate: any other exception may have resulted from user code, and should not be translated. + * @param ex runtime exception that occurred + * @return the corresponding DataAccessException instance, or {@literal null} if the exception should not be translated + * * * @author Oliver Gierke */ public class MongoExceptionTranslator implements PersistenceExceptionTranslator { - /* - * (non-Javadoc) - * - * @see org.springframework.dao.support.PersistenceExceptionTranslator# - * translateExceptionIfPossible(java.lang.RuntimeException) - */ - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + /* + * (non-Javadoc) + * + * @see org.springframework.dao.support.PersistenceExceptionTranslator# + * translateExceptionIfPossible(java.lang.RuntimeException) + */ + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { - return MongoDbUtils.translateMongoExceptionIfPossible(ex); - } + // Check for well-known MongoException subclasses. + + // All other MongoExceptions + if (ex instanceof DuplicateKey) { + return new DataIntegrityViolationException(ex.getMessage(), ex); + } + if (ex instanceof Network) { + return new DataAccessResourceFailureException(ex.getMessage(), ex); + } + if (ex instanceof MongoException) { + return new UncategorizedDocumentStoreException(ex.getMessage(), ex); + } + + // If we get here, we have an exception that resulted from user code, + // rather than the persistence provider, so we return null to indicate + // that translation should not occur. + return null; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoFactoryBean.java index 4fee05697..9346aaa55 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoFactoryBean.java @@ -22,8 +22,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.dao.DataAccessException; -import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.util.Assert; import com.mongodb.Mongo; @@ -38,8 +36,7 @@ import com.mongodb.ServerAddress; * * @since 1.0 */ -public class MongoFactoryBean implements FactoryBean, InitializingBean, - PersistenceExceptionTranslator { +public class MongoFactoryBean implements FactoryBean, InitializingBean { /** @@ -120,10 +117,4 @@ public class MongoFactoryBean implements FactoryBean, InitializingBean, } } } - - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { - logger.debug("Translating " + ex); - return MongoDbUtils.translateMongoExceptionIfPossible(ex); - } - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoOperations.java index 9d6167929..4771cd624 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoOperations.java @@ -16,6 +16,7 @@ package org.springframework.data.document.mongodb; import java.util.List; +import java.util.Set; import com.mongodb.DBCollection; import com.mongodb.DBObject; @@ -27,7 +28,7 @@ import com.mongodb.DBObject; * * @author Thomas Risberg * @author Mark Pollack - * + * @author Oliver Gierke */ public interface MongoOperations { @@ -38,7 +39,7 @@ public interface MongoOperations { String getDefaultCollectionName(); /** - * The default collection used by this template + * The default collection used by this template. * @return The default collection used by this template */ DBCollection getDefaultCollection(); @@ -117,17 +118,17 @@ public interface MongoOperations { DBCollection createCollection(String collectionName); /** - * Create a collect with the provided name and options + * Create a collect with the provided name and options. * @param collectionName name of the collection * @param collectionOptions options to use when creating the collection. */ void createCollection(String collectionName, CollectionOptions collectionOptions); /** - * A list of collection names + * A set of collection names. * @return list of collection names */ - List getCollectionNames(); + Set getCollectionNames(); /** * Get a collection by name, creating it if it doesn't exist. @@ -216,14 +217,14 @@ public interface MongoOperations { * * @param listToSave the list of objects to save. */ - void insertList(List listToSave); + void insertList(List listToSave); /** * Insert a list of objects into the specified collection in a single batch write to the database. * @param collectionName name of the collection to store the object in * @param listToSave the list of objects to save. */ - void insertList(String collectionName, List listToSave); + void insertList(String collectionName, List listToSave); /** * Insert a list of objects into the specified collection using the provided MongoWriter instance @@ -233,7 +234,7 @@ public interface MongoOperations { * @param listToSave the list of objects to save. * @param writer the writer to convert the object to save into a DBObject */ - void insertList(String collectionName, List listToSave, MongoWriter writer); + void insertList(String collectionName, List listToSave, MongoWriter writer); /** * Save the object to the default collection. This will perform an insert if the object is not already diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoTemplate.java index 343b4bc0f..031f9b515 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/MongoTemplate.java @@ -16,19 +16,20 @@ package org.springframework.data.document.mongodb; -import static java.lang.String.*; - import java.beans.PropertyDescriptor; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Set; import org.bson.types.ObjectId; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.beans.factory.InitializingBean; -import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jca.cci.core.ConnectionCallback; import org.springframework.util.Assert; import com.mongodb.BasicDBObject; @@ -39,7 +40,6 @@ import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.mongodb.Mongo; import com.mongodb.MongoException; -import com.mongodb.WriteResult; import com.mongodb.util.JSON; /** @@ -53,10 +53,10 @@ import com.mongodb.util.JSON; public class MongoTemplate implements InitializingBean, MongoOperations { private static final String ID = "_id"; - private static final String COLLECTION_ERROR_TEMPLATE = "Error creating collection %s: %s"; private final MongoConverter mongoConverter; private final Mongo mongo; + private final MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); private String defaultCollectionName; private String databaseName; @@ -98,7 +98,7 @@ public class MongoTemplate implements InitializingBean, MongoOperations { } /** - * Sets the password to use to authenticate with the Mongo database + * Sets the password to use to authenticate with the Mongo database. * * @param password The password to use */ @@ -107,10 +107,20 @@ public class MongoTemplate implements InitializingBean, MongoOperations { this.password = password; } + /** + * Sets the name of the default collection to be used. + * + * @param defaultCollectionName + */ public void setDefaultCollectionName(String defaultCollectionName) { this.defaultCollectionName = defaultCollectionName; } + /** + * Sets the database name to be used. + * + * @param databaseName + */ public void setDatabaseName(String databaseName) { Assert.notNull(databaseName); this.databaseName = databaseName; @@ -127,7 +137,12 @@ public class MongoTemplate implements InitializingBean, MongoOperations { * @see org.springframework.data.document.mongodb.MongoOperations#getDefaultCollection() */ public DBCollection getDefaultCollection() { - return getDb().getCollection(getDefaultCollectionName()); + + return execute(new DBCallback() { + public DBCollection doInDB(DB db) throws MongoException, DataAccessException { + return db.getCollection(getDefaultCollectionName()); + } + }); } /* (non-Javadoc) @@ -140,33 +155,41 @@ public class MongoTemplate implements InitializingBean, MongoOperations { /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#executeCommand(com.mongodb.DBObject) */ - public void executeCommand(DBObject command) { - CommandResult cr = getDb().command(command); - String err = cr.getErrorMessage(); - if (err != null) { - throw new InvalidDataAccessApiUsageException("Command execution of " + - command.toString() + " failed: " + err); + public void executeCommand(final DBObject command) { + + CommandResult result = execute(new DBCallback() { + public CommandResult doInDB(DB db) throws MongoException, DataAccessException { + return db.command(command); + } + }); + + String error = result.getErrorMessage(); + if (error != null) { + throw new InvalidDataAccessApiUsageException("Command execution of " + + command.toString() + " failed: " + error); } } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#execute(org.springframework.data.document.mongodb.DBCallback) */ - public T execute(DBCallback action) { - DB db = getDb(); - + public T execute(DbCallback action) { + + Assert.notNull(action); + try { + DB db = getDb(); return action.doInDB(db); } catch (MongoException e) { - throw MongoDbUtils.translateMongoExceptionIfPossible(e); + throw potentiallyConvertRuntimeException(e); } } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#execute(org.springframework.data.document.mongodb.CollectionCallback) */ - public T execute(CollectionCallback action) { - return execute(action, defaultCollectionName); + public T execute(CollectionCallback callback) { + return execute(callback, defaultCollectionName); } /* (non-Javadoc) @@ -174,79 +197,126 @@ public class MongoTemplate implements InitializingBean, MongoOperations { */ public T execute(CollectionCallback callback, String collectionName) { + Assert.notNull(callback); + try { - return callback.doInCollection(getCollection(collectionName)); + DBCollection collection = getDb().getCollection(collectionName); + return callback.doInCollection(collection); } catch (MongoException e) { - throw MongoDbUtils.translateMongoExceptionIfPossible(e); + throw potentiallyConvertRuntimeException(e); + } + } + + /** + * Central callback executing method to do queries against the datastore that requires reading a collection of + * objects. It will take the following steps
  1. Execute the given {@link ConnectionCallback} for a + * {@link DBCursor}.
  2. Prepare that {@link DBCursor} with the given {@link CursorPreparer} (will be skipped + * if {@link CursorPreparer} is {@literal null}
  3. Iterate over the {@link DBCursor} and applies the given + * {@link DbObjectCallback} to each of the {@link DBObject}s collecting the actual result {@link List}.
    1. + * + * @param + * @param collectionCallback the callback to retrieve the {@link DBCursor} with + * @param preparer the {@link CursorPreparer} to potentially modify the {@link DBCursor} before ireating over it + * @param objectCallback the {@link DbObjectCallback} to transform {@link DBObject}s into the actual domain type + * @param collectionName the collection to be queried + * @return + */ + private List executeEach(CollectionCallback collectionCallback, CursorPreparer preparer, + DbObjectCallback objectCallback, String collectionName) { + + try { + DBCursor cursor = collectionCallback.doInCollection(getCollection(collectionName)); + + if (preparer != null) { + preparer.prepare(cursor); + } + + List result = new ArrayList(); + + for (DBObject object : cursor) { + result.add(objectCallback.doWith(object)); + } + + return result; + } catch (MongoException e) { + throw potentiallyConvertRuntimeException(e); } } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#executeInSession(org.springframework.data.document.mongodb.DBCallback) */ - public T executeInSession(DBCallback action) { - DB db = getDb(); - db.requestStart(); - try { - return action.doInDB(db); - } catch (MongoException e) { - throw MongoDbUtils.translateMongoExceptionIfPossible(e); - } finally { - db.requestDone(); - } + public T executeInSession(final DBCallback action) { + + return execute(new DBCallback() { + public T doInDB(DB db) throws MongoException, DataAccessException { + try { + db.requestStart(); + return action.doInDB(db); + } finally { + db.requestDone(); + } + } + }); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#createCollection(java.lang.String) */ - public DBCollection createCollection(String collectionName) { - try { - return getDb().createCollection(collectionName, new BasicDBObject()); - } catch (MongoException e) { - throw new InvalidDataAccessApiUsageException(format(COLLECTION_ERROR_TEMPLATE, collectionName, e.getMessage()), e); - } + public DBCollection createCollection(final String collectionName) { + return execute(new DBCallback() { + public DBCollection doInDB(DB db) throws MongoException, DataAccessException { + return db.createCollection(collectionName, new BasicDBObject()); + } + }); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#createCollection(java.lang.String, org.springframework.data.document.mongodb.CollectionOptions) */ - public void createCollection(String collectionName, CollectionOptions collectionOptions) { - try { - getDb().createCollection(collectionName, convertToDbObject(collectionOptions)); - } catch (MongoException e) { - throw new InvalidDataAccessApiUsageException(format(COLLECTION_ERROR_TEMPLATE, collectionName, e.getMessage()), e); - } + public void createCollection(final String collectionName, final CollectionOptions collectionOptions) { + execute(new DBCallback() { + public Void doInDB(DB db) throws MongoException, DataAccessException { + db.createCollection(collectionName, convertToDbObject(collectionOptions)); + return null; + } + }); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#getCollection(java.lang.String) */ - public DBCollection getCollection(String collectionName) { - try { - return getDb().getCollection(collectionName); - } catch (MongoException e) { - throw new InvalidDataAccessApiUsageException(format(COLLECTION_ERROR_TEMPLATE, collectionName, e.getMessage()), e); - } + public DBCollection getCollection(final String collectionName) { + return execute(new DBCallback() { + public DBCollection doInDB(DB db) throws MongoException, DataAccessException { + return db.getCollection(collectionName); + } + }); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#collectionExists(java.lang.String) */ - public boolean collectionExists(String collectionName) { - try { - return getDb().collectionExists(collectionName); - } catch (MongoException e) { - throw new InvalidDataAccessApiUsageException("Error creating collection " + collectionName + ": " + e.getMessage(), e); - } + public boolean collectionExists(final String collectionName) { + return execute(new DBCallback() { + public Boolean doInDB(DB db) throws MongoException, DataAccessException { + return db.collectionExists(collectionName); + } + }); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#dropCollection(java.lang.String) */ public void dropCollection(String collectionName) { - getDb().getCollection(collectionName) - .drop(); + + execute(new CollectionCallback() { + public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException { + collection.drop(); + return null; + } + }, collectionName); } @@ -287,21 +357,24 @@ public class MongoTemplate implements InitializingBean, MongoOperations { /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#insertList(java.util.List) */ - public void insertList(List listToSave) { + public void insertList(List listToSave) { insertList(getRequiredDefaultCollectionName(), listToSave); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#insertList(java.lang.String, java.util.List) */ - public void insertList(String collectionName, List listToSave) { + public void insertList(String collectionName, List listToSave) { insertList(collectionName, listToSave, this.mongoConverter); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#insertList(java.lang.String, java.util.List, org.springframework.data.document.mongodb.MongoWriter) */ - public void insertList(String collectionName, List listToSave, MongoWriter writer) { + public void insertList(String collectionName, List listToSave, MongoWriter writer) { + + Assert.notNull(writer); + List dbObjectList = new ArrayList(); for (T o : listToSave) { BasicDBObject dbDoc = new BasicDBObject(); @@ -341,51 +414,56 @@ public class MongoTemplate implements InitializingBean, MongoOperations { } - protected Object insertDBObject(String collectionName, DBObject dbDoc) { - if (dbDoc.keySet().size() > 0 ) { - WriteResult wr = null; - try { - wr = getDb().getCollection(collectionName).insert(dbDoc); - return dbDoc.get(ID); - } catch (MongoException e) { - throw new DataRetrievalFailureException(wr.getLastError().getErrorMessage(), e); - } - } - else { + protected Object insertDBObject(String collectionName, final DBObject dbDoc) { + + if (dbDoc.keySet().isEmpty()) { return null; } + + return execute(new CollectionCallback() { + public Object doInCollection(DBCollection collection) throws MongoException, DataAccessException { + collection.insert(dbDoc); + return dbDoc.get(ID); + } + }, collectionName); + } + + + + + protected List insertDBObjectList(String collectionName, final List dbDocList) { + + if (dbDocList.isEmpty()) { + return Collections.emptyList(); + } + + execute(new CollectionCallback() { + public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException { + collection.insert(dbDocList); + return null; + } + }, collectionName); + + List ids = new ArrayList(); + for (DBObject dbo : dbDocList) { + ids.add(dbo.get(ID)); + } + return ids; } - protected List insertDBObjectList(String collectionName, List dbDocList) { - if (!dbDocList.isEmpty()) { - List ids = new ArrayList(); - WriteResult wr = null; - try { - wr = getDb().getCollection(collectionName).insert(dbDocList); - for (DBObject dbo : dbDocList) { - ids.add(dbo.get(ID)); - } - return ids; - } catch (MongoException e) { - throw new DataRetrievalFailureException(wr.getLastError().getErrorMessage(), e); - } - } else { + protected Object saveDBObject(String collectionName, final DBObject dbDoc) { + + if (dbDoc.keySet().isEmpty()) { return null; } - } + + return execute(new CollectionCallback() { - protected Object saveDBObject(String collectionName, DBObject dbDoc) { - if (dbDoc.keySet().size() > 0 ) { - WriteResult wr = null; - try { - wr = getDb().getCollection(collectionName).save(dbDoc); + public Object doInCollection(DBCollection collection) throws MongoException, DataAccessException { + collection.save(dbDoc); return dbDoc.get(ID); - } catch (MongoException e) { - throw new DataRetrievalFailureException(wr.getLastError().getErrorMessage(), e); } - } else { - return null; - } + }, collectionName); } /* (non-Javadoc) @@ -398,13 +476,13 @@ public class MongoTemplate implements InitializingBean, MongoOperations { /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#updateFirst(java.lang.String, com.mongodb.DBObject, com.mongodb.DBObject) */ - public void updateFirst(String collectionName, DBObject queryDoc, DBObject updateDoc) { - - try { - getDb().getCollection(collectionName).update(queryDoc, updateDoc); - } catch (MongoException e) { - throw new DataRetrievalFailureException("Error during update using " + queryDoc + ", " + updateDoc + "!", e); - } + public void updateFirst(String collectionName, final DBObject queryDoc, final DBObject updateDoc) { + execute(new CollectionCallback() { + public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException { + collection.update(queryDoc, updateDoc); + return null; + } + }, collectionName); } /* (non-Javadoc) @@ -417,12 +495,13 @@ public class MongoTemplate implements InitializingBean, MongoOperations { /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#updateMulti(java.lang.String, com.mongodb.DBObject, com.mongodb.DBObject) */ - public void updateMulti(String collectionName, DBObject queryDoc, DBObject updateDoc) { - try { - getDb().getCollection(collectionName).updateMulti(queryDoc, updateDoc); - } catch (MongoException e) { - throw new DataRetrievalFailureException("Error during updateMulti using " + queryDoc + ", " + updateDoc + "!", e); - } + public void updateMulti(String collectionName, final DBObject queryDoc, final DBObject updateDoc) { + execute(new CollectionCallback() { + public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException { + collection.updateMulti(queryDoc, updateDoc); + return null; + } + }, collectionName); } /* (non-Javadoc) @@ -435,12 +514,13 @@ public class MongoTemplate implements InitializingBean, MongoOperations { /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#remove(java.lang.String, com.mongodb.DBObject) */ - public void remove(String collectionName, DBObject queryDoc) { - try { - getDb().getCollection(collectionName).remove(queryDoc); - } catch (MongoException e) { - throw new DataRetrievalFailureException("Error during remove using " + queryDoc + "!", e); - } + public void remove(String collectionName, final DBObject queryDoc) { + execute(new CollectionCallback() { + public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException { + collection.remove(queryDoc); + return null; + } + }, collectionName); } @@ -448,53 +528,35 @@ public class MongoTemplate implements InitializingBean, MongoOperations { * @see org.springframework.data.document.mongodb.MongoOperations#getCollection(java.lang.Class) */ public List getCollection(Class targetClass) { - - List results = new ArrayList(); - DBCollection collection = getDb().getCollection(getDefaultCollectionName()); - for (DBObject dbo : collection.find()) { - Object obj = mongoConverter.read(targetClass, dbo); - //effectively acts as a query on the collection restricting it to elements of a specific type - if (targetClass.isInstance(obj)) { - results.add(targetClass.cast(obj)); - } - } - return results; + return executeEach(new FindCallback(null), null, new ReadDbObjectCallback(mongoConverter, targetClass), + getDefaultCollectionName()); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#getCollection(java.lang.String, java.lang.Class) */ public List getCollection(String collectionName, Class targetClass) { - - List results = new ArrayList(); - DBCollection collection = getDb().getCollection(collectionName); - for (DBObject dbo : collection.find()) { - Object obj = mongoConverter.read(targetClass, dbo); - //effectively acts as a query on the collection restricting it to elements of a specific type - if (targetClass.isInstance(obj)) { - results.add(targetClass.cast(obj)); - } - } - return results; + return executeEach(new FindCallback(null), null, new ReadDbObjectCallback(mongoConverter, targetClass), + collectionName); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#getCollectionNames() */ - public List getCollectionNames() { - return new ArrayList(getDb().getCollectionNames()); + public Set getCollectionNames() { + return execute(new DBCallback>() { + public Set doInDB(DB db) throws MongoException, DataAccessException { + return db.getCollectionNames(); + } + }); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#getCollection(java.lang.String, java.lang.Class, org.springframework.data.document.mongodb.MongoReader) */ - public List getCollection(String collectionName, Class targetClass, MongoReader reader) { - List results = new ArrayList(); - DBCollection collection = getDb().getCollection(collectionName); - for (DBObject dbo : collection.find()) { - results.add(reader.read(targetClass, dbo)); - } - return results; + public List getCollection(String collectionName, Class targetClass, MongoReader reader) { + return executeEach(new FindCallback(null), null, new ReadDbObjectCallback(reader, targetClass), + collectionName); } // Queries that take JavaScript to express the query. @@ -504,7 +566,7 @@ public class MongoTemplate implements InitializingBean, MongoOperations { */ public List queryUsingJavaScript(String query, Class targetClass) { return query(getDefaultCollectionName(), (DBObject)JSON.parse(query), targetClass); // - } + } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#queryUsingJavaScript(java.lang.String, java.lang.Class, org.springframework.data.document.mongodb.MongoReader) @@ -554,7 +616,7 @@ public class MongoTemplate implements InitializingBean, MongoOperations { /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#query(java.lang.String, com.mongodb.DBObject, java.lang.Class) */ - public List query(String collectionName, DBObject query, Class targetClass) { + public List query(String collectionName, DBObject query, Class targetClass) { return query(collectionName, query, targetClass, (CursorPreparer) null); } @@ -562,37 +624,18 @@ public class MongoTemplate implements InitializingBean, MongoOperations { * @see org.springframework.data.document.mongodb.MongoOperations#query(java.lang.String, com.mongodb.DBObject, java.lang.Class, org.springframework.data.document.mongodb.CursorPreparer) */ public List query(String collectionName, DBObject query, Class targetClass, CursorPreparer preparer) { - DBCollection collection = getDb().getCollection(collectionName); - List results = new ArrayList(); - DBCursor cursor = collection.find(query); - if (preparer != null) { - preparer.prepare(cursor); - } - for (DBObject dbo : cursor) { - Object obj = mongoConverter.read(targetClass,dbo); - //effectively acts as a query on the collection restricting it to elements of a specific type - if (targetClass.isInstance(obj)) { - results.add(targetClass.cast(obj)); - } - } - return results; + return executeEach(new FindCallback(query), preparer, new ReadDbObjectCallback(mongoConverter, targetClass), + collectionName); } /* (non-Javadoc) * @see org.springframework.data.document.mongodb.MongoOperations#query(java.lang.String, com.mongodb.DBObject, java.lang.Class, org.springframework.data.document.mongodb.MongoReader) */ public List query(String collectionName, DBObject query, Class targetClass, MongoReader reader) { - DBCollection collection = getDb().getCollection(collectionName); - List results = new ArrayList(); - for (DBObject dbo : collection.find(query)) { - results.add(reader.read(targetClass, dbo)); - } - return results; + return executeEach(new FindCallback(query), null, new ReadDbObjectCallback(reader, targetClass), + collectionName); } - public RuntimeException convertMongoAccessException(RuntimeException ex) { - return MongoDbUtils.translateMongoExceptionIfPossible(ex); - } public DB getDb() { return MongoDbUtils.getDB(mongo, databaseName, username, password == null ? null : password.toCharArray()); @@ -603,7 +646,7 @@ public class MongoTemplate implements InitializingBean, MongoOperations { DBObject dbo = new BasicDBObject(); if (collectionOptions != null) { if (collectionOptions.getCapped() != null) { - dbo.put("capped", collectionOptions.getCapped().booleanValue()); + dbo.put("capped", collectionOptions.getCapped().booleanValue()); } if (collectionOptions.getSize() != null) { dbo.put("size", collectionOptions.getSize().intValue()); @@ -634,6 +677,19 @@ public class MongoTemplate implements InitializingBean, MongoOperations { } } } + + /** + * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original + * exception if the conversation failed. Thus allows safe rethrowing of the return value. + * + * @param ex + * @return + */ + private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) { + + RuntimeException resolved = this.exceptionTranslator.translateExceptionIfPossible(ex); + return resolved == null ? ex : resolved; + } /* * (non-Javadoc) @@ -641,10 +697,62 @@ public class MongoTemplate implements InitializingBean, MongoOperations { */ public void afterPropertiesSet() { if (this.getDefaultCollectionName() != null) { - DB db = getDb(); - if (! db.collectionExists(getDefaultCollectionName())) { - db.createCollection(getDefaultCollectionName(), null); + if (!collectionExists(getDefaultCollectionName())) { + createCollection(getDefaultCollectionName(), null); } } } + + + /** + * Simple {@link CollectionCallback} that takes a query {@link DBObject} and executes that against the + * {@link DBCollection}. + * + * @author Oliver Gierke + */ + private static class FindCallback implements CollectionCallback { + + private final DBObject query; + + public FindCallback(DBObject query) { + this.query = query; + } + + public DBCursor doInCollection(DBCollection collection) throws MongoException, DataAccessException { + return collection.find(query); + } + } + + /** + * Simple internal callback to allow operations on a {@link DBObject}. + * + * @author Oliver Gierke + */ + + private interface DbObjectCallback { + + T doWith(DBObject object); + } + + /** + * Simple {@link DbObjectCallback} that will transform {@link DBObject} into the given target type using the given + * {@link MongoReader}. + * + * @author Oliver Gierke + */ + private static class ReadDbObjectCallback implements DbObjectCallback { + + private final MongoReader reader; + private final Class type; + + public ReadDbObjectCallback(MongoReader reader, Class type) { + this.reader = reader; + this.type = type; + } + + @SuppressWarnings("unchecked") + public T doWith(DBObject object) { + return (T) reader.read(type, object); + } + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoOperationsUnitTests.java new file mode 100644 index 000000000..c75f2e90a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoOperationsUnitTests.java @@ -0,0 +1,344 @@ +/* + * Copyright 2011 the original author or authors. + * + * 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 org.springframework.data.document.mongodb; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.dao.DataAccessException; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Abstract base class for unit tests to specify behaviour we expect from {@link MongoOperations}. Subclasses return + * instances of their implementation and thus can see if it correctly implements the {@link MongoOperations} interface. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public abstract class MongoOperationsUnitTests { + + @Mock + CollectionCallback collectionCallback; + @Mock + DbCallback dbCallback; + + MongoConverter converter; + Person person; + List persons; + + @Before + public final void operationsSetUp() { + + person = new Person("Oliver"); + persons = Arrays.asList(person); + + converter = new MongoConverter() { + + public Object read(Class clazz, DBObject dbo) { + return person; + } + + public void write(Object t, DBObject dbo) { + dbo.put("firstName", person.getFirstName()); + } + }; + } + + + @Test(expected = IllegalArgumentException.class) + @SuppressWarnings({"unchecked", "rawtypes"}) + public void rejectsNullForCollectionCallback() { + + getOperations().execute((CollectionCallback) null); + } + + @Test(expected = IllegalArgumentException.class) + @SuppressWarnings({"unchecked", "rawtypes"}) + public void rejectsNullForCollectionCallback2() { + getOperations().execute((CollectionCallback) null, "collection"); + } + + @Test(expected = IllegalArgumentException.class) + @SuppressWarnings({"unchecked", "rawtypes"}) + public void rejectsNullForDbCallback() { + getOperations().execute((DbCallback) null); + } + + @Test + public void convertsExceptionForCollectionExists() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.collectionExists("foo"); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForCreateCollection() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.createCollection("foo"); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForCreateCollection2() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.createCollection("foo", new CollectionOptions(1, 1, true)); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForDropCollection() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.dropCollection("foo"); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForExecuteCollectionCallback() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.execute(collectionCallback); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForExecuteDbCallback() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.execute(dbCallback); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForExecuteCollectionCallbackAndCollection() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.execute(collectionCallback, "collection"); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForExecuteCommand() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.executeCommand(new BasicDBObject()); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForExecuteStringCommand() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.executeCommand(""); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForExecuteInSession() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.executeInSession(dbCallback); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForGetCollection() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.getCollection(Object.class); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForGetCollectionWithCollectionName() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.getCollection("collection"); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForGetCollectionWithCollectionNameAndType() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.getCollection("collection", Object.class); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForGetCollectionWithCollectionNameTypeAndReader() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.getCollection("collection", Object.class, converter); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForGetCollectionNames() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.getCollectionNames(); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForGetDefaultCollection() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.getDefaultCollection(); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForInsert() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.insert(person); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForInsert2() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.insert("collection", person); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForInsert3() { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.insert("collection", person, converter); + } + }.assertDataAccessException(); + } + + + @Test + public void convertsExceptionForInsertList() throws Exception { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.insertList(persons); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForGetInsertList2() throws Exception { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.insertList("collection", persons); + } + }.assertDataAccessException(); + } + + @Test + public void convertsExceptionForGetInsertList3() throws Exception { + new Execution() { + @Override + public void doWith(MongoOperations operations) { + operations.insertList("collection", persons, converter); + } + }.assertDataAccessException(); + } + + private abstract class Execution { + + public void assertDataAccessException() { + assertException(DataAccessException.class); + } + + public void assertException(Class exception) { + + try { + doWith(getOperationsForExceptionHandling()); + fail("Expected " + exception + " but completed without any!"); + } catch (Exception e) { + assertTrue("Expected " + exception + " but got " + e, exception.isInstance(e)); + } + } + + public abstract void doWith(MongoOperations operations); + } + + /** + * Expects an {@link MongoOperations} instance that will be used to check that invoking methods on it will only + * cause {@link DataAccessException}s. + * + * @return + */ + protected abstract MongoOperations getOperationsForExceptionHandling(); + + /** + * Returns a plain {@link MongoOperations}. + * + * @return + */ + protected abstract MongoOperations getOperations(); +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoTemplateUnitTests.java index b9b0e45a6..b3b0814e6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoTemplateUnitTests.java @@ -23,7 +23,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.DataAccessException; import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.DB; @@ -36,7 +36,7 @@ import com.mongodb.MongoException; * @author Oliver Gierke */ @RunWith(MockitoJUnitRunner.class) -public class MongoTemplateUnitTests { +public class MongoTemplateUnitTests extends MongoOperationsUnitTests { MongoTemplate template; @@ -48,7 +48,7 @@ public class MongoTemplateUnitTests { @Before public void setUp() { - this.template = new MongoTemplate(mongo, "database"); + this.template = new MongoTemplate(mongo, "database", "default"); } @Test(expected = IllegalArgumentException.class) @@ -61,7 +61,7 @@ public class MongoTemplateUnitTests { new MongoTemplate(null, "database"); } - @Test(expected = DataRetrievalFailureException.class) + @Test(expected = DataAccessException.class) public void removeHandlesMongoExceptionProperly() throws Exception { MongoTemplate template = mockOutGetDb(); when(db.getCollection("collection")).thenThrow(new MongoException("Exception!")); @@ -87,4 +87,22 @@ public class MongoTemplateUnitTests { stub(template.getDb()).toReturn(db); return template; } + + /* (non-Javadoc) + * @see org.springframework.data.document.mongodb.MongoOperationsUnitTests#getOperations() + */ + @Override + protected MongoOperations getOperationsForExceptionHandling() { + MongoTemplate template = spy(this.template); + stub(template.getDb()).toThrow(new MongoException("Error!")); + return template; + } + + /* (non-Javadoc) + * @see org.springframework.data.document.mongodb.MongoOperationsUnitTests#getOperations() + */ + @Override + protected MongoOperations getOperations() { + return this.template; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/Person.java index 01b134f64..a3b86c433 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/Person.java @@ -20,6 +20,14 @@ public class Person { private String firstName; private Person friend; + + public Person() { + + } + + public Person(String firstname) { + this.firstName = firstname; + } public String getFirstName() {