/*
* Copyright 2010-2017 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.mongodb.core;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import static org.springframework.data.util.Optionals.*;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.Fields;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
import org.springframework.data.mongodb.core.mapreduce.GroupBy;
import org.springframework.data.mongodb.core.mapreduce.GroupByResults;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.mapreduce.MapReduceResults;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Meta;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.util.MongoClientVersion;
import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Pair;
import org.springframework.jca.cci.core.ConnectionCallback;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import com.mongodb.Cursor;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MapReduceIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.util.JSONParseException;
/**
* Primary implementation of {@link MongoOperations}.
*
* @author Thomas Risberg
* @author Graeme Rocher
* @author Mark Pollack
* @author Oliver Gierke
* @author Amol Nayak
* @author Patryk Wasik
* @author Tobias Trelle
* @author Sebastian Herold
* @author Thomas Darimont
* @author Chuong Ngo
* @author Christoph Strobl
* @author Doménique Tilleuil
* @author Niko Schmuck
* @author Mark Paluch
* @author Laszlo Csontos
* @author Maninder Singh
*/
@SuppressWarnings("deprecation")
public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(MongoTemplate.class);
private static final String ID_FIELD = "_id";
private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE;
private static final Collection<String> ITERABLE_CLASSES;
static {
Set<String> iterableClasses = new HashSet<String>();
iterableClasses.add(List.class.getName());
iterableClasses.add(Collection.class.getName());
iterableClasses.add(Iterator.class.getName());
ITERABLE_CLASSES = Collections.unmodifiableCollection(iterableClasses);
}
private final MongoConverter mongoConverter;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final MongoDbFactory mongoDbFactory;
private final PersistenceExceptionTranslator exceptionTranslator;
private final QueryMapper queryMapper;
private final UpdateMapper updateMapper;
private WriteConcern writeConcern;
private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE;
private WriteResultChecking writeResultChecking = WriteResultChecking.NONE;
private ReadPreference readPreference;
private ApplicationEventPublisher eventPublisher;
private ResourceLoader resourceLoader;
private MongoPersistentEntityIndexCreator indexCreator;
/**
* Constructor used for a basic template configuration
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
*/
public MongoTemplate(MongoClient mongoClient, String databaseName) {
this(new SimpleMongoDbFactory(mongoClient, databaseName), null);
}
/**
* Constructor used for a basic template configuration.
*
* @param mongoDbFactory must not be {@literal null}.
*/
public MongoTemplate(MongoDbFactory mongoDbFactory) {
this(mongoDbFactory, null);
}
/**
* Constructor used for a basic template configuration.
*
* @param mongoDbFactory must not be {@literal null}.
* @param mongoConverter
*/
public MongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter mongoConverter) {
Assert.notNull(mongoDbFactory, "MongoDbFactory must not be null!");
this.mongoDbFactory = mongoDbFactory;
this.exceptionTranslator = mongoDbFactory.getExceptionTranslator();
this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter(mongoDbFactory) : mongoConverter;
this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter);
// We always have a mapping context in the converter, whether it's a simple one or not
mappingContext = this.mongoConverter.getMappingContext();
// We create indexes based on mapping events
if (null != mappingContext && mappingContext instanceof MongoMappingContext) {
indexCreator = new MongoPersistentEntityIndexCreator((MongoMappingContext) mappingContext, this);
eventPublisher = new MongoMappingEventPublisher(indexCreator);
if (mappingContext instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
}
}
}
/**
* Configures the {@link WriteResultChecking} to be used with the template. Setting {@literal null} will reset the
* default of {@link #DEFAULT_WRITE_RESULT_CHECKING}.
*
* @param resultChecking
*/
public void setWriteResultChecking(WriteResultChecking resultChecking) {
this.writeResultChecking = resultChecking == null ? DEFAULT_WRITE_RESULT_CHECKING : resultChecking;
}
/**
* Configures the {@link WriteConcern} to be used with the template. If none is configured the {@link WriteConcern}
* configured on the {@link MongoDbFactory} will apply. If you configured a {@link Mongo} instance no
* {@link WriteConcern} will be used.
*
* @param writeConcern
*/
public void setWriteConcern(WriteConcern writeConcern) {
this.writeConcern = writeConcern;
}
/**
* Configures the {@link WriteConcernResolver} to be used with the template.
*
* @param writeConcernResolver
*/
public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) {
this.writeConcernResolver = writeConcernResolver;
}
/**
* Used by @{link {@link #prepareCollection(MongoCollection)} to set the {@link ReadPreference} before any operations
* are performed.
*
* @param readPreference
*/
public void setReadPreference(ReadPreference readPreference) {
this.readPreference = readPreference;
}
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
prepareIndexCreator(applicationContext);
eventPublisher = applicationContext;
if (mappingContext instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
}
resourceLoader = applicationContext;
}
/**
* Inspects the given {@link ApplicationContext} for {@link MongoPersistentEntityIndexCreator} and those in turn if
* they were registered for the current {@link MappingContext}. If no creator for the current {@link MappingContext}
* can be found we manually add the internally created one as {@link ApplicationListener} to make sure indexes get
* created appropriately for entity types persisted through this {@link MongoTemplate} instance.
*
* @param context must not be {@literal null}.
*/
private void prepareIndexCreator(ApplicationContext context) {
String[] indexCreators = context.getBeanNamesForType(MongoPersistentEntityIndexCreator.class);
for (String creator : indexCreators) {
MongoPersistentEntityIndexCreator creatorBean = context.getBean(creator, MongoPersistentEntityIndexCreator.class);
if (creatorBean.isIndexCreatorFor(mappingContext)) {
return;
}
}
if (context instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) context).addApplicationListener(indexCreator);
}
}
/**
* Returns the default {@link org.springframework.data.mongodb.core.convert.MongoConverter}.
*
* @return
*/
public MongoConverter getConverter() {
return this.mongoConverter;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#executeAsStream(org.springframework.data.mongodb.core.query.Query, java.lang.Class)
*/
@Override
public <T> CloseableIterator<T> stream(final Query query, final Class<T> entityType) {
return stream(query, entityType, determineCollectionName(entityType));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#stream(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String)
*/
@Override
public <T> CloseableIterator<T> stream(final Query query, final Class<T> entityType, final String collectionName) {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(entityType, "Entity type must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
return execute(collectionName, new CollectionCallback<CloseableIterator<T>>() {
@Override
public CloseableIterator<T> doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
MongoPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entityType);
Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), persistentEntity);
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), persistentEntity);
FindIterable<Document> cursor = new QueryCursorPreparer(query, entityType)
.prepare(collection.find(mappedQuery).projection(mappedFields));
return new CloseableIterableCursorAdapter<T>(cursor, exceptionTranslator,
new ReadDocumentCallback<T>(mongoConverter, entityType, collectionName));
}
});
}
public String getCollectionName(Class<?> entityClass) {
return this.determineCollectionName(entityClass);
}
public Document executeCommand(final String jsonCommand) {
return execute(new DbCallback<Document>() {
public Document doInDB(MongoDatabase db) throws MongoException, DataAccessException {
return db.runCommand(Document.parse(jsonCommand), Document.class);
}
});
}
public Document executeCommand(final Document command) {
Document result = execute(new DbCallback<Document>() {
public Document doInDB(MongoDatabase db) throws MongoException, DataAccessException {
return db.runCommand(command, Document.class);
}
});
return result;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#executeCommand(com.mongodb.Document, com.mongodb.ReadPreference)
*/
public Document executeCommand(final Document command, final ReadPreference readPreference) {
Assert.notNull(command, "Command must not be null!");
Document result = execute(new DbCallback<Document>() {
public Document doInDB(MongoDatabase db) throws MongoException, DataAccessException {
return readPreference != null ? db.runCommand(command, readPreference, Document.class)
: db.runCommand(command, Document.class);
}
});
return result;
}
public void executeQuery(Query query, String collectionName, DocumentCallbackHandler dch) {
executeQuery(query, collectionName, dch, new QueryCursorPreparer(query, null));
}
/**
* Execute a MongoDB query and iterate over the query results on a per-document basis with a
* {@link DocumentCallbackHandler} using the provided CursorPreparer.
*
* @param query the query class that specifies the criteria used to find a record and also an optional fields
* specification, must not be {@literal null}.
* @param collectionName name of the collection to retrieve the objects from
* @param dch the handler that will extract results, one document at a time
* @param preparer allows for customization of the {@link DBCursor} used when iterating over the result set, (apply
* limits, skips and so on).
*/
protected void executeQuery(Query query, String collectionName, DocumentCallbackHandler dch,
CursorPreparer preparer) {
Assert.notNull(query, "Query must not be null!");
Document queryObject = queryMapper.getMappedObject(query.getQueryObject(), Optional.empty());
Document sortObject = query.getSortObject();
Document fieldsObject = query.getFieldsObject();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing query: {} sort: {} fields: {} in collection: {}", serializeToJsonSafely(queryObject),
sortObject, fieldsObject, collectionName);
}
this.executeQueryInternal(new FindCallback(queryObject, fieldsObject), preparer, dch, collectionName);
}
public <T> T execute(DbCallback<T> action) {
Assert.notNull(action, "DbCallbackmust not be null!");
try {
MongoDatabase db = this.getDb();
return action.doInDB(db);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
}
public <T> T execute(Class<?> entityClass, CollectionCallback<T> callback) {
return execute(determineCollectionName(entityClass), callback);
}
public <T> T execute(String collectionName, CollectionCallback<T> callback) {
Assert.notNull(callback, "CollectionCallback must not be null!");
try {
MongoCollection<Document> collection = getAndPrepareCollection(getDb(), collectionName);
return callback.doInCollection(collection);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
}
public <T> MongoCollection<Document> createCollection(Class<T> entityClass) {
return createCollection(determineCollectionName(entityClass));
}
public <T> MongoCollection<Document> createCollection(Class<T> entityClass, CollectionOptions collectionOptions) {
return createCollection(determineCollectionName(entityClass), collectionOptions);
}
public MongoCollection<Document> createCollection(final String collectionName) {
return doCreateCollection(collectionName, new Document());
}
public MongoCollection<Document> createCollection(final String collectionName,
final CollectionOptions collectionOptions) {
return doCreateCollection(collectionName, convertToDocument(collectionOptions));
}
public MongoCollection<Document> getCollection(final String collectionName) {
return execute(new DbCallback<MongoCollection<Document>>() {
public MongoCollection<Document> doInDB(MongoDatabase db) throws MongoException, DataAccessException {
return db.getCollection(collectionName, Document.class);
}
});
}
public <T> boolean collectionExists(Class<T> entityClass) {
return collectionExists(determineCollectionName(entityClass));
}
public boolean collectionExists(final String collectionName) {
return execute(new DbCallback<Boolean>() {
public Boolean doInDB(MongoDatabase db) throws MongoException, DataAccessException {
for (String name : db.listCollectionNames()) {
if (name.equals(collectionName)) {
return true;
}
}
return false;
}
});
}
public <T> void dropCollection(Class<T> entityClass) {
dropCollection(determineCollectionName(entityClass));
}
public void dropCollection(String collectionName) {
execute(collectionName, new CollectionCallback<Void>() {
public Void doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
collection.drop();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Dropped collection [{}]", collection.getNamespace().getCollectionName());
}
return null;
}
});
}
public IndexOperations indexOps(String collectionName) {
return new DefaultIndexOperations(getMongoDbFactory(), collectionName, queryMapper);
}
public IndexOperations indexOps(Class<?> entityClass) {
return new DefaultIndexOperations(getMongoDbFactory(), determineCollectionName(entityClass), queryMapper);
}
public BulkOperations bulkOps(BulkMode bulkMode, String collectionName) {
return bulkOps(bulkMode, null, collectionName);
}
public BulkOperations bulkOps(BulkMode bulkMode, Class<?> entityClass) {
return bulkOps(bulkMode, entityClass, determineCollectionName(entityClass));
}
public BulkOperations bulkOps(BulkMode mode, Class<?> entityType, String collectionName) {
Assert.notNull(mode, "BulkMode must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
DefaultBulkOperations operations = new DefaultBulkOperations(this, mode, collectionName, entityType);
operations.setExceptionTranslator(exceptionTranslator);
operations.setWriteConcernResolver(writeConcernResolver);
operations.setDefaultWriteConcern(writeConcern);
return operations;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#scriptOps()
*/
@Override
public ScriptOperations scriptOps() {
return new DefaultScriptOperations(this);
}
// Find methods that take a Query to express the query and that return a single object.
public <T> T findOne(Query query, Class<T> entityClass) {
return findOne(query, entityClass, determineCollectionName(entityClass));
}
public <T> T findOne(Query query, Class<T> entityClass, String collectionName) {
if (query.getSortObject() == null) {
return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass);
} else {
query.limit(1);
List<T> results = find(query, entityClass, collectionName);
return results.isEmpty() ? null : results.get(0);
}
}
public boolean exists(Query query, Class<?> entityClass) {
return exists(query, entityClass, determineCollectionName(entityClass));
}
public boolean exists(Query query, String collectionName) {
return exists(query, null, collectionName);
}
public boolean exists(Query query, Class<?> entityClass, String collectionName) {
if (query == null) {
throw new InvalidDataAccessApiUsageException("Query passed in to exist can't be null");
}
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), getPersistentEntity(entityClass));
FindIterable<Document> iterable = execute(collectionName, new FindCallback(mappedQuery));
if (query.getCollation().isPresent()) {
iterable = iterable
.collation(query.getCollation().map(org.springframework.data.mongodb.core.Collation::toMongoCollation).get());
}
return iterable.iterator().hasNext();
}
// Find methods that take a Query to express the query and that return a List of objects.
public <T> List<T> find(Query query, Class<T> entityClass) {
return find(query, entityClass, determineCollectionName(entityClass));
}
public <T> List<T> find(final Query query, Class<T> entityClass, String collectionName) {
if (query == null) {
return findAll(entityClass, collectionName);
}
return doFind(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass,
new QueryCursorPreparer(query, entityClass));
}
public <T> T findById(Object id, Class<T> entityClass) {
return findById(id, entityClass, determineCollectionName(entityClass));
}
public <T> T findById(Object id, Class<T> entityClass, String collectionName) {
String idKey = mappingContext.getPersistentEntity(entityClass)//
.flatMap(it -> it.getIdProperty())//
.map(it -> it.getName()).orElse(ID_FIELD);
return doFindOne(collectionName, new Document(idKey, id), null, entityClass);
}
public <T> GeoResults<T> geoNear(NearQuery near, Class<T> entityClass) {
return geoNear(near, entityClass, determineCollectionName(entityClass));
}
@SuppressWarnings("unchecked")
public <T> GeoResults<T> geoNear(NearQuery near, Class<T> entityClass, String collectionName) {
if (near == null) {
throw new InvalidDataAccessApiUsageException("NearQuery must not be null!");
}
if (entityClass == null) {
throw new InvalidDataAccessApiUsageException("Entity class must not be null!");
}
String collection = StringUtils.hasText(collectionName) ? collectionName : determineCollectionName(entityClass);
Document nearDocument = near.toDocument();
Document command = new Document("geoNear", collection);
command.putAll(nearDocument);
if (nearDocument.containsKey("query")) {
Document query = (Document) nearDocument.get("query");
command.put("query", queryMapper.getMappedObject(query, getPersistentEntity(entityClass)));
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing geoNear using: {} for class: {} in collection: {}", serializeToJsonSafely(command),
entityClass, collectionName);
}
Document commandResult = executeCommand(command, this.readPreference);
List<Object> results = (List<Object>) commandResult.get("results");
results = results == null ? Collections.emptyList() : results;
DocumentCallback<GeoResult<T>> callback = new GeoNearResultDocumentCallback<T>(
new ReadDocumentCallback<T>(mongoConverter, entityClass, collectionName), near.getMetric());
List<GeoResult<T>> result = new ArrayList<GeoResult<T>>(results.size());
int index = 0;
long elementsToSkip = near.getSkip() != null ? near.getSkip() : 0;
for (Object element : results) {
/*
* As MongoDB currently (2.4.4) doesn't support the skipping of elements in near queries
* we skip the elements ourselves to avoid at least the document 2 object mapping overhead.
*
* @see <a href="https://jira.mongodb.org/browse/SERVER-3925">MongoDB Jira: SERVER-3925</a>
*/
if (index >= elementsToSkip) {
result.add(callback.doWith((Document) element));
}
index++;
}
if (elementsToSkip > 0) {
// as we skipped some elements we have to calculate the averageDistance ourselves:
return new GeoResults<T>(result, near.getMetric());
}
GeoCommandStatistics stats = GeoCommandStatistics.from(commandResult);
return new GeoResults<T>(result, new Distance(stats.getAverageDistance(), near.getMetric()));
}
public <T> T findAndModify(Query query, Update update, Class<T> entityClass) {
return findAndModify(query, update, new FindAndModifyOptions(), entityClass, determineCollectionName(entityClass));
}
public <T> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName) {
return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName);
}
public <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass) {
return findAndModify(query, update, options, entityClass, determineCollectionName(entityClass));
}
public <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName) {
FindAndModifyOptions optionsToUse = FindAndModifyOptions.of(options);
Optionals.ifAllPresent(query.getCollation(), optionsToUse.getCollation(), (l, r) -> {
throw new IllegalArgumentException(
"Both Query and FindAndModifyOptions define a collation. Please provide the collation only via one of the two.");
});
query.getCollation().ifPresent(optionsToUse::collation);
return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(),
getMappedSortObject(query, entityClass), entityClass, update, optionsToUse);
}
// Find methods that take a Query to express the query and that return a single object that is also removed from the
// collection in the database.
public <T> T findAndRemove(Query query, Class<T> entityClass) {
return findAndRemove(query, entityClass, determineCollectionName(entityClass));
}
public <T> T findAndRemove(Query query, Class<T> entityClass, String collectionName) {
return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(),
getMappedSortObject(query, entityClass), query.getCollation().orElse(null), entityClass);
}
public long count(Query query, Class<?> entityClass) {
Assert.notNull(entityClass, "Entity class must not be null!");
return count(query, entityClass, determineCollectionName(entityClass));
}
public long count(final Query query, String collectionName) {
return count(query, null, collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#count(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String)
*/
public long count(Query query, Class<?> entityClass, String collectionName) {
Assert.hasText(collectionName, "Collection name must not be null or empty!");
final Document document = query == null ? null
: queryMapper.getMappedObject(query.getQueryObject(),
Optional.ofNullable(entityClass).flatMap(it -> mappingContext.getPersistentEntity(entityClass)));
return execute(collectionName, (CollectionCallback<Long>) collection -> collection.count(document));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#insert(java.lang.Object)
*/
public void insert(Object objectToSave) {
ensureNotIterable(objectToSave);
insert(objectToSave, determineEntityCollectionName(objectToSave));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#insert(java.lang.Object, java.lang.String)
*/
public void insert(Object objectToSave, String collectionName) {
ensureNotIterable(objectToSave);
doInsert(collectionName, objectToSave, this.mongoConverter);
}
protected void ensureNotIterable(Object o) {
if (null != o) {
if (o.getClass().isArray() || ITERABLE_CLASSES.contains(o.getClass().getName())) {
throw new IllegalArgumentException("Cannot use a collection here.");
}
}
}
/**
* Prepare the collection before any processing is done using it. This allows a convenient way to apply settings like
* slaveOk() etc. Can be overridden in sub-classes.
*
* @param collection
*/
protected MongoCollection<Document> prepareCollection(MongoCollection<Document> collection) {
if (this.readPreference != null) {
return collection.withReadPreference(readPreference);
}
return collection;
}
/**
* Prepare the WriteConcern before any processing is done using it. This allows a convenient way to apply custom
* settings in sub-classes. <br />
* In case of using MongoDB Java driver version 3 the returned {@link WriteConcern} will be defaulted to
* {@link WriteConcern#ACKNOWLEDGED} when {@link WriteResultChecking} is set to {@link WriteResultChecking#EXCEPTION}.
*
* @param writeConcern any WriteConcern already configured or null
* @return The prepared WriteConcern or null
*/
protected WriteConcern prepareWriteConcern(MongoAction mongoAction) {
WriteConcern wc = writeConcernResolver.resolve(mongoAction);
return potentiallyForceAcknowledgedWrite(wc);
}
private WriteConcern potentiallyForceAcknowledgedWrite(WriteConcern wc) {
if (ObjectUtils.nullSafeEquals(WriteResultChecking.EXCEPTION, writeResultChecking)
&& MongoClientVersion.isMongo3Driver()) {
if (wc == null || wc.getWObject() == null
|| (wc.getWObject() instanceof Number && ((Number) wc.getWObject()).intValue() < 1)) {
return WriteConcern.ACKNOWLEDGED;
}
}
return wc;
}
protected <T> void doInsert(String collectionName, T objectToSave, MongoWriter<T> writer) {
initializeVersionProperty(objectToSave);
maybeEmitEvent(new BeforeConvertEvent<T>(objectToSave, collectionName));
assertUpdateableIdIfNotSet(objectToSave);
Document dbDoc = toDocument(objectToSave, writer);
maybeEmitEvent(new BeforeSaveEvent<T>(objectToSave, dbDoc, collectionName));
Object id = insertDocument(collectionName, dbDoc, objectToSave.getClass());
populateIdIfNecessary(objectToSave, id);
maybeEmitEvent(new AfterSaveEvent<T>(objectToSave, dbDoc, collectionName));
}
/**
* @param objectToSave
* @param writer
* @return
*/
private <T> Document toDocument(T objectToSave, MongoWriter<T> writer) {
if (objectToSave instanceof Document) {
return (Document) objectToSave;
}
if (!(objectToSave instanceof String)) {
Document dbDoc = new Document();
writer.write(objectToSave, dbDoc);
if (dbDoc.containsKey(ID_FIELD) && dbDoc.get(ID_FIELD) == null) {
dbDoc.remove(ID_FIELD);
}
return dbDoc;
} else {
try {
return Document.parse((String) objectToSave);
} catch (JSONParseException e) {
throw new MappingException("Could not parse given String to save into a JSON document!", e);
} catch (org.bson.json.JsonParseException e) {
throw new MappingException("Could not parse given String to save into a JSON document!", e);
}
}
}
private void initializeVersionProperty(Object entity) {
Optional<? extends MongoPersistentEntity<?>> persistentEntity = getPersistentEntity(entity.getClass());
ifAllPresent(persistentEntity, persistentEntity.flatMap(PersistentEntity::getVersionProperty), (l, r) -> {
ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(l.getPropertyAccessor(entity),
mongoConverter.getConversionService());
accessor.setProperty(r, Optional.of(0));
});
}
public void insert(Collection<? extends Object> batchToSave, Class<?> entityClass) {
doInsertBatch(determineCollectionName(entityClass), batchToSave, this.mongoConverter);
}
public void insert(Collection<? extends Object> batchToSave, String collectionName) {
doInsertBatch(collectionName, batchToSave, this.mongoConverter);
}
public void insertAll(Collection<? extends Object> objectsToSave) {
doInsertAll(objectsToSave, this.mongoConverter);
}
protected <T> void doInsertAll(Collection<? extends T> listToSave, MongoWriter<T> writer) {
Map<String, List<T>> elementsByCollection = new HashMap<String, List<T>>();
for (T element : listToSave) {
if (element == null) {
continue;
}
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(element.getClass());
String collection = entity.getCollection();
List<T> collectionElements = elementsByCollection.get(collection);
if (null == collectionElements) {
collectionElements = new ArrayList<T>();
elementsByCollection.put(collection, collectionElements);
}
collectionElements.add(element);
}
for (Map.Entry<String, List<T>> entry : elementsByCollection.entrySet()) {
doInsertBatch(entry.getKey(), entry.getValue(), this.mongoConverter);
}
}
protected <T> void doInsertBatch(String collectionName, Collection<? extends T> batchToSave, MongoWriter<T> writer) {
Assert.notNull(writer, "MongoWriter must not be null!");
List<Document> documentList = new ArrayList<Document>();
for (T o : batchToSave) {
initializeVersionProperty(o);
maybeEmitEvent(new BeforeConvertEvent<T>(o, collectionName));
Document document = toDocument(o, writer);
maybeEmitEvent(new BeforeSaveEvent<T>(o, document, collectionName));
documentList.add(document);
}
List<Object> ids = consolidateIdentifiers(insertDocumentList(collectionName, documentList), documentList);
int i = 0;
for (T obj : batchToSave) {
if (i < ids.size()) {
populateIdIfNecessary(obj, ids.get(i));
maybeEmitEvent(new AfterSaveEvent<T>(obj, documentList.get(i), collectionName));
}
i++;
}
}
public void save(Object objectToSave) {
Assert.notNull(objectToSave, "Object to save must not be null!");
save(objectToSave, determineEntityCollectionName(objectToSave));
}
public void save(Object objectToSave, String collectionName) {
Assert.notNull(objectToSave, "Object to save must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
Optional<? extends MongoPersistentEntity<?>> entity = getPersistentEntity(objectToSave.getClass());
Optional<MongoPersistentProperty> versionProperty = entity.flatMap(PersistentEntity::getVersionProperty);
mapIfAllPresent(entity, versionProperty, //
(l, r) -> doSaveVersioned(objectToSave, l, collectionName))//
.orElseGet(() -> doSave(collectionName, objectToSave, this.mongoConverter));
}
private <T> T doSaveVersioned(T objectToSave, MongoPersistentEntity<?> entity, String collectionName) {
ConvertingPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor(
entity.getPropertyAccessor(objectToSave), mongoConverter.getConversionService());
Optional<MongoPersistentProperty> versionProperty = entity.getVersionProperty();
Optional<Number> versionNumber = versionProperty.flatMap(it -> convertingAccessor.getProperty(it, Number.class));
return mapIfAllPresent(versionProperty, versionNumber, (property, number) -> {
// Bump version number
convertingAccessor.setProperty(property, Optional.of(number.longValue() + 1));
maybeEmitEvent(new BeforeConvertEvent<T>(objectToSave, collectionName));
assertUpdateableIdIfNotSet(objectToSave);
Document document = new Document();
this.mongoConverter.write(objectToSave, document);
maybeEmitEvent(new BeforeSaveEvent<T>(objectToSave, document, collectionName));
Update update = Update.fromDocument(document, ID_FIELD);
// Create query for entity with the id and old version
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
Object id = entity.getIdentifierAccessor(objectToSave).getRequiredIdentifier();
Query query = new Query(Criteria.where(idProperty.getName()).is(id).and(property.getName()).is(number));
UpdateResult result = doUpdate(collectionName, query, update, objectToSave.getClass(), false, false);
if (result.getModifiedCount() == 0) {
throw new OptimisticLockingFailureException(
String.format("Cannot save entity %s with version %s to collection %s. Has it been modified meanwhile?", id,
versionNumber, collectionName));
}
maybeEmitEvent(new AfterSaveEvent<T>(objectToSave, document, collectionName));
return objectToSave;
}).orElseGet(() -> {
doInsert(collectionName, objectToSave, this.mongoConverter);
return objectToSave;
});
}
protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
maybeEmitEvent(new BeforeConvertEvent<T>(objectToSave, collectionName));
assertUpdateableIdIfNotSet(objectToSave);
Document dbDoc = toDocument(objectToSave, writer);
maybeEmitEvent(new BeforeSaveEvent<T>(objectToSave, dbDoc, collectionName));
Object id = saveDocument(collectionName, dbDoc, objectToSave.getClass());
populateIdIfNecessary(objectToSave, id);
maybeEmitEvent(new AfterSaveEvent<T>(objectToSave, dbDoc, collectionName));
return objectToSave;
}
protected Object insertDocument(final String collectionName, final Document document, final Class<?> entityClass) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Inserting Document containing fields: {} in collection: {}", document.keySet(), collectionName);
}
return execute(collectionName, new CollectionCallback<Object>() {
public Object doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName,
entityClass, document, null);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
if (writeConcernToUse == null) {
collection.insertOne(document);
} else {
collection.withWriteConcern(writeConcernToUse).insertOne(document);
}
return document.get(ID_FIELD);
}
});
}
// TODO: 2.0 - Change method signature to return List<Object> and return all identifiers (DATAMONGO-1513,
// DATAMONGO-1519)
protected List<ObjectId> insertDocumentList(final String collectionName, final List<Document> documents) {
if (documents.isEmpty()) {
return Collections.emptyList();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Inserting list of Documents containing {} items", documents.size());
}
execute(collectionName, new CollectionCallback<Void>() {
public Void doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT_LIST, collectionName, null,
null, null);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
if (writeConcernToUse == null) {
collection.insertMany(documents);
} else {
collection.withWriteConcern(writeConcernToUse).insertMany(documents);
}
return null;
}
});
List<ObjectId> ids = new ArrayList<ObjectId>();
for (Document dbo : documents) {
Object id = dbo.get(ID_FIELD);
if (id instanceof ObjectId) {
ids.add((ObjectId) id);
} else {
// no id was generated
ids.add(null);
}
}
return ids;
}
protected Object saveDocument(final String collectionName, final Document dbDoc, final Class<?> entityClass) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Saving Document containing fields: {}", dbDoc.keySet());
}
return execute(collectionName, new CollectionCallback<Object>() {
public Object doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass,
dbDoc, null);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
if (!dbDoc.containsKey(ID_FIELD)) {
if (writeConcernToUse == null) {
collection.insertOne(dbDoc);
} else {
collection.withWriteConcern(writeConcernToUse).insertOne(dbDoc);
}
}
else if (writeConcernToUse == null) {
collection.replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, new UpdateOptions().upsert(true));
} else {
collection.withWriteConcern(writeConcernToUse).replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc,
new UpdateOptions().upsert(true));
}
return dbDoc.get(ID_FIELD);
}
});
}
public UpdateResult upsert(Query query, Update update, Class<?> entityClass) {
return doUpdate(determineCollectionName(entityClass), query, update, entityClass, true, false);
}
public UpdateResult upsert(Query query, Update update, String collectionName) {
return doUpdate(collectionName, query, update, null, true, false);
}
public UpdateResult upsert(Query query, Update update, Class<?> entityClass, String collectionName) {
return doUpdate(collectionName, query, update, entityClass, true, false);
}
public UpdateResult updateFirst(Query query, Update update, Class<?> entityClass) {
return doUpdate(determineCollectionName(entityClass), query, update, entityClass, false, false);
}
public UpdateResult updateFirst(final Query query, final Update update, final String collectionName) {
return doUpdate(collectionName, query, update, null, false, false);
}
public UpdateResult updateFirst(Query query, Update update, Class<?> entityClass, String collectionName) {
return doUpdate(collectionName, query, update, entityClass, false, false);
}
public UpdateResult updateMulti(Query query, Update update, Class<?> entityClass) {
return doUpdate(determineCollectionName(entityClass), query, update, entityClass, false, true);
}
public UpdateResult updateMulti(final Query query, final Update update, String collectionName) {
return doUpdate(collectionName, query, update, null, false, true);
}
public UpdateResult updateMulti(final Query query, final Update update, Class<?> entityClass, String collectionName) {
return doUpdate(collectionName, query, update, entityClass, false, true);
}
protected UpdateResult doUpdate(final String collectionName, final Query query, final Update update,
final Class<?> entityClass, final boolean upsert, final boolean multi) {
return execute(collectionName, new CollectionCallback<UpdateResult>() {
public UpdateResult doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
Optional<? extends MongoPersistentEntity<?>> entity = entityClass == null ? Optional.empty()
: getPersistentEntity(entityClass);
increaseVersionForUpdateIfNecessary(entity, update);
UpdateOptions opts = new UpdateOptions();
opts.upsert(upsert);
Document queryObj = new Document();
if (query != null) {
queryObj.putAll(queryMapper.getMappedObject(query.getQueryObject(), entity));
query.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation);
}
Document updateObj = update == null ? new Document()
: updateMapper.getMappedObject(update.getUpdateObject(), entity);
if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) {
queryObj.put("$isolated", 1);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Calling update using query: {} and update: {} in collection: {}",
serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName);
}
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName,
entityClass, updateObj, queryObj);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
collection = writeConcernToUse != null ? collection.withWriteConcern(writeConcernToUse) : collection;
if (!UpdateMapper.isUpdateObject(updateObj)) {
return collection.replaceOne(queryObj, updateObj, opts);
} else {
if (multi) {
return collection.updateMany(queryObj, updateObj, opts);
} else {
return collection.updateOne(queryObj, updateObj, opts);
}
}
}
});
}
private void increaseVersionForUpdateIfNecessary(Optional<? extends MongoPersistentEntity<?>> persistentEntity,
Update update) {
ifAllPresent(persistentEntity, persistentEntity.flatMap(PersistentEntity::getVersionProperty),
(entity, property) -> {
String versionFieldName = property.getFieldName();
if (!update.modifies(versionFieldName)) {
update.inc(versionFieldName, 1L);
}
});
}
private boolean documentContainsVersionProperty(Document document,
Optional<? extends MongoPersistentEntity<?>> persistentEntity) {
return mapIfAllPresent(persistentEntity, persistentEntity.flatMap(PersistentEntity::getVersionProperty), //
(entity, property) -> document.containsKey(property.getFieldName()))//
.orElse(false);
}
public DeleteResult remove(Object object) {
if (object == null) {
return null;
}
return remove(getIdQueryFor(object), object.getClass());
}
public DeleteResult remove(Object object, String collection) {
Assert.hasText(collection, "Collection name must not be null or empty!");
if (object == null) {
return null;
}
return doRemove(collection, getIdQueryFor(object), object.getClass());
}
/**
* Returns {@link Entry} containing the field name of the id property as {@link Entry#getKey()} and the {@link Id}s
* property value as its {@link Entry#getValue()}.
*
* @param object
* @return
*/
private Pair<String, Optional<Object>> extractIdPropertyAndValue(Object object) {
Assert.notNull(object, "Id cannot be extracted from 'null'.");
Class<?> objectType = object.getClass();
if (object instanceof Document) {
return Pair.of(ID_FIELD, Optional.ofNullable(((Document) object).get(ID_FIELD)));
}
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(objectType);
return mapIfAllPresent(entity, entity.flatMap(it -> it.getIdProperty()), //
(l, r) -> Pair.of(r.getFieldName(), l.getPropertyAccessor(object).getProperty(r)))//
.orElseThrow(() -> new MappingException("No id property found for object of type " + objectType));
}
/**
* Returns a {@link Query} for the given entity by its id.
*
* @param object must not be {@literal null}.
* @return
*/
private Query getIdQueryFor(Object object) {
Pair<String, Optional<Object>> id = extractIdPropertyAndValue(object);
return new Query(where(id.getFirst()).is(id.getSecond().orElse(null)));
}
/**
* Returns a {@link Query} for the given entities by their ids.
*
* @param objects must not be {@literal null} or {@literal empty}.
* @return
*/
private Query getIdInQueryFor(Collection<?> objects) {
Assert.notEmpty(objects, "Cannot create Query for empty collection.");
Iterator<?> it = objects.iterator();
Pair<String, Optional<Object>> pair = extractIdPropertyAndValue(it.next());
ArrayList<Object> ids = new ArrayList<Object>(objects.size());
ids.add(pair.getSecond().orElse(null));
while (it.hasNext()) {
ids.add(extractIdPropertyAndValue(it.next()).getSecond().orElse(null));
}
return new Query(where(pair.getFirst()).in(ids));
}
private void assertUpdateableIdIfNotSet(Object value) {
Optional<? extends MongoPersistentEntity<?>> persistentEntity = mappingContext
.getPersistentEntity(value.getClass());
Optional<MongoPersistentProperty> idProperty = persistentEntity.flatMap(it -> it.getIdProperty());
Optionals.ifAllPresent(persistentEntity, idProperty, (entity, property) -> {
Optional<Object> propertyValue = entity.getPropertyAccessor(value).getProperty(property);
if (propertyValue.isPresent()) {
return;
}
if (!MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(property.getType())) {
throw new InvalidDataAccessApiUsageException(
String.format("Cannot autogenerate id of type %s for entity of type %s!", property.getType().getName(),
value.getClass().getName()));
}
});
}
public DeleteResult remove(Query query, String collectionName) {
return remove(query, null, collectionName);
}
public DeleteResult remove(Query query, Class<?> entityClass) {
return remove(query, entityClass, determineCollectionName(entityClass));
}
public DeleteResult remove(Query query, Class<?> entityClass, String collectionName) {
return doRemove(collectionName, query, entityClass);
}
protected <T> DeleteResult doRemove(final String collectionName, final Query query, final Class<T> entityClass) {
if (query == null) {
throw new InvalidDataAccessApiUsageException("Query passed in to remove can't be null!");
}
Assert.hasText(collectionName, "Collection name must not be null or empty!");
final Document queryObject = query.getQueryObject();
final Optional<? extends MongoPersistentEntity<?>> entity = getPersistentEntity(entityClass);
return execute(collectionName, new CollectionCallback<DeleteResult>() {
public DeleteResult doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
maybeEmitEvent(new BeforeDeleteEvent<T>(queryObject, entityClass, collectionName));
Document mappedQuery = queryMapper.getMappedObject(queryObject, entity);
DeleteOptions options = new DeleteOptions();
query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName,
entityClass, null, queryObject);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
DeleteResult dr = null;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Remove using query: {} in collection: {}.",
new Object[] { serializeToJsonSafely(mappedQuery), collectionName });
}
if (writeConcernToUse == null) {
dr = collection.deleteMany(mappedQuery, options);
} else {
dr = collection.withWriteConcern(writeConcernToUse).deleteMany(mappedQuery, options);
}
maybeEmitEvent(new AfterDeleteEvent<T>(queryObject, entityClass, collectionName));
return dr;
}
});
}
public <T> List<T> findAll(Class<T> entityClass) {
return findAll(entityClass, determineCollectionName(entityClass));
}
public <T> List<T> findAll(Class<T> entityClass, String collectionName) {
return executeFindMultiInternal(new FindCallback(null, null), null,
new ReadDocumentCallback<T>(mongoConverter, entityClass, collectionName), collectionName);
}
public <T> MapReduceResults<T> mapReduce(String inputCollectionName, String mapFunction, String reduceFunction,
Class<T> entityClass) {
return mapReduce(null, inputCollectionName, mapFunction, reduceFunction, new MapReduceOptions().outputTypeInline(),
entityClass);
}
public <T> MapReduceResults<T> mapReduce(String inputCollectionName, String mapFunction, String reduceFunction,
MapReduceOptions mapReduceOptions, Class<T> entityClass) {
return mapReduce(null, inputCollectionName, mapFunction, reduceFunction, mapReduceOptions, entityClass);
}
public <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, String mapFunction,
String reduceFunction, Class<T> entityClass) {
return mapReduce(query, inputCollectionName, mapFunction, reduceFunction, new MapReduceOptions().outputTypeInline(),
entityClass);
}
public <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, String mapFunction,
String reduceFunction, MapReduceOptions mapReduceOptions, Class<T> entityClass) {
String mapFunc = replaceWithResourceIfNecessary(mapFunction);
String reduceFunc = replaceWithResourceIfNecessary(reduceFunction);
MongoCollection<Document> inputCollection = getCollection(inputCollectionName);
// MapReduceOp
MapReduceIterable<Document> result = inputCollection.mapReduce(mapFunc, reduceFunc);
if (query != null && result != null) {
if (query.getLimit() > 0 && mapReduceOptions.getLimit() == null) {
result = result.limit(query.getLimit());
}
if (query.getMeta() != null && query.getMeta().getMaxTimeMsec() != null) {
result = result.maxTime(query.getMeta().getMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
if (query.getSortObject() != null) {
result = result.sort(query.getSortObject());
}
result = result.filter(queryMapper.getMappedObject(query.getQueryObject(), Optional.empty()));
}
Optional<Collation> collation = query != null ? query.getCollation() : Optional.empty();
if (mapReduceOptions != null) {
Optionals.ifAllPresent(collation, mapReduceOptions.getCollation(), (l, r) -> {
throw new IllegalArgumentException(
"Both Query and MapReduceOptions define a collation. Please provide the collation only via one of the two.");
});
if (mapReduceOptions.getCollation().isPresent()) {
collation = mapReduceOptions.getCollation();
}
if (!CollectionUtils.isEmpty(mapReduceOptions.getScopeVariables())) {
result = result.scope(new Document(mapReduceOptions.getScopeVariables()));
}
if (mapReduceOptions.getLimit() != null && mapReduceOptions.getLimit().intValue() > 0) {
result = result.limit(mapReduceOptions.getLimit());
}
if (mapReduceOptions.getFinalizeFunction().filter(StringUtils::hasText).isPresent()) {
result = result.finalizeFunction(mapReduceOptions.getFinalizeFunction().get());
}
if (mapReduceOptions.getJavaScriptMode() != null) {
result = result.jsMode(mapReduceOptions.getJavaScriptMode());
}
if (mapReduceOptions.getOutputSharded().isPresent()) {
result = result.sharded(mapReduceOptions.getOutputSharded().get());
}
}
result = collation.map(Collation::toMongoCollation).map(result::collation).orElse(result);
List<T> mappedResults = new ArrayList<T>();
DocumentCallback<T> callback = new ReadDocumentCallback<T>(mongoConverter, entityClass, inputCollectionName);
for (Document document : result) {
mappedResults.add(callback.doWith(document));
}
return new MapReduceResults<T>(mappedResults, new Document());
}
public <T> GroupByResults<T> group(String inputCollectionName, GroupBy groupBy, Class<T> entityClass) {
return group(null, inputCollectionName, groupBy, entityClass);
}
public <T> GroupByResults<T> group(Criteria criteria, String inputCollectionName, GroupBy groupBy,
Class<T> entityClass) {
Document document = groupBy.getGroupByObject();
document.put("ns", inputCollectionName);
if (criteria == null) {
document.put("cond", null);
} else {
document.put("cond", queryMapper.getMappedObject(criteria.getCriteriaObject(), Optional.empty()));
}
// If initial document was a JavaScript string, potentially loaded by Spring's Resource abstraction, load it and
// convert to Document
if (document.containsKey("initial")) {
Object initialObj = document.get("initial");
if (initialObj instanceof String) {
String initialAsString = replaceWithResourceIfNecessary((String) initialObj);
document.put("initial", Document.parse(initialAsString));
}
}
if (document.containsKey("$reduce")) {
document.put("$reduce", replaceWithResourceIfNecessary(document.get("$reduce").toString()));
}
if (document.containsKey("$keyf")) {
document.put("$keyf", replaceWithResourceIfNecessary(document.get("$keyf").toString()));
}
if (document.containsKey("finalize")) {
document.put("finalize", replaceWithResourceIfNecessary(document.get("finalize").toString()));
}
Document commandObject = new Document("group", document);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing Group with Document [{}]", serializeToJsonSafely(commandObject));
}
Document commandResult = executeCommand(commandObject);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Group command result = [{}]", commandResult);
}
@SuppressWarnings("unchecked")
Iterable<Document> resultSet = (Iterable<Document>) commandResult.get("retval");
List<T> mappedResults = new ArrayList<T>();
DocumentCallback<T> callback = new ReadDocumentCallback<T>(mongoConverter, entityClass, inputCollectionName);
for (Document resultDocument : resultSet) {
mappedResults.add(callback.doWith(resultDocument));
}
return new GroupByResults<T>(mappedResults, commandResult);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#aggregate(org.springframework.data.mongodb.core.aggregation.TypedAggregation, java.lang.Class)
*/
@Override
public <O> AggregationResults<O> aggregate(TypedAggregation<?> aggregation, Class<O> outputType) {
return aggregate(aggregation, determineCollectionName(aggregation.getInputType()), outputType);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#aggregate(org.springframework.data.mongodb.core.aggregation.TypedAggregation, java.lang.String, java.lang.Class)
*/
@Override
public <O> AggregationResults<O> aggregate(TypedAggregation<?> aggregation, String inputCollectionName,
Class<O> outputType) {
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
AggregationOperationContext context = new TypeBasedAggregationOperationContext(aggregation.getInputType(),
mappingContext, queryMapper);
return aggregate(aggregation, inputCollectionName, outputType, context);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#aggregate(org.springframework.data.mongodb.core.aggregation.Aggregation, java.lang.Class, java.lang.Class)
*/
@Override
public <O> AggregationResults<O> aggregate(Aggregation aggregation, Class<?> inputType, Class<O> outputType) {
return aggregate(aggregation, determineCollectionName(inputType), outputType,
new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper));
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#aggregate(org.springframework.data.mongodb.core.aggregation.Aggregation, java.lang.String, java.lang.Class)
*/
@Override
public <O> AggregationResults<O> aggregate(Aggregation aggregation, String collectionName, Class<O> outputType) {
return aggregate(aggregation, collectionName, outputType, null);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#aggregateStream(org.springframework.data.mongodb.core.aggregation.TypedAggregation, java.lang.String, java.lang.Class)
*/
@Override
public <O> CloseableIterator<O> aggregateStream(TypedAggregation<?> aggregation, String inputCollectionName,
Class<O> outputType) {
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
AggregationOperationContext context = new TypeBasedAggregationOperationContext(aggregation.getInputType(),
mappingContext, queryMapper);
return aggregateStream(aggregation, inputCollectionName, outputType, context);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#aggregateStream(org.springframework.data.mongodb.core.aggregation.TypedAggregation, java.lang.Class)
*/
@Override
public <O> CloseableIterator<O> aggregateStream(TypedAggregation<?> aggregation, Class<O> outputType) {
return aggregateStream(aggregation, determineCollectionName(aggregation.getInputType()), outputType);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#aggregateStream(org.springframework.data.mongodb.core.aggregation.Aggregation, java.lang.Class, java.lang.Class)
*/
@Override
public <O> CloseableIterator<O> aggregateStream(Aggregation aggregation, Class<?> inputType, Class<O> outputType) {
return aggregateStream(aggregation, determineCollectionName(inputType), outputType,
new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper));
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#aggregateStream(org.springframework.data.mongodb.core.aggregation.Aggregation, java.lang.String, java.lang.Class)
*/
@Override
public <O> CloseableIterator<O> aggregateStream(Aggregation aggregation, String collectionName, Class<O> outputType) {
return aggregateStream(aggregation, collectionName, outputType, null);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#findAllAndRemove(org.springframework.data.mongodb.core.query.Query, java.lang.String)
*/
@Override
public <T> List<T> findAllAndRemove(Query query, String collectionName) {
return findAndRemove(query, null, collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#findAllAndRemove(org.springframework.data.mongodb.core.query.Query, java.lang.Class)
*/
@Override
public <T> List<T> findAllAndRemove(Query query, Class<T> entityClass) {
return findAllAndRemove(query, entityClass, determineCollectionName(entityClass));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#findAllAndRemove(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String)
*/
@Override
public <T> List<T> findAllAndRemove(Query query, Class<T> entityClass, String collectionName) {
return doFindAndDelete(collectionName, query, entityClass);
}
/**
* Retrieve and remove all documents matching the given {@code query} by calling {@link #find(Query, Class, String)}
* and {@link #remove(Query, Class, String)}, whereas the {@link Query} for {@link #remove(Query, Class, String)} is
* constructed out of the find result.
*
* @param collectionName
* @param query
* @param entityClass
* @return
*/
protected <T> List<T> doFindAndDelete(String collectionName, Query query, Class<T> entityClass) {
List<T> result = find(query, entityClass, collectionName);
if (!CollectionUtils.isEmpty(result)) {
remove(getIdInQueryFor(result), entityClass, collectionName);
}
return result;
}
protected <O> AggregationResults<O> aggregate(Aggregation aggregation, String collectionName, Class<O> outputType,
AggregationOperationContext context) {
Assert.hasText(collectionName, "Collection name must not be null or empty!");
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
Assert.notNull(outputType, "Output type must not be null!");
AggregationOperationContext rootContext = context == null ? Aggregation.DEFAULT_CONTEXT : context;
Document command = aggregation.toDocument(collectionName, rootContext);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing aggregation: {}", serializeToJsonSafely(command));
}
Document commandResult = executeCommand(command, this.readPreference);
return new AggregationResults<O>(returnPotentiallyMappedResults(outputType, commandResult, collectionName),
commandResult);
}
/**
* Returns the potentially mapped results of the given {@code commandResult}.
*
* @param outputType
* @param commandResult
* @return
*/
private <O> List<O> returnPotentiallyMappedResults(Class<O> outputType, Document commandResult,
String collectionName) {
@SuppressWarnings("unchecked")
Iterable<Document> resultSet = (Iterable<Document>) commandResult.get("result");
if (resultSet == null) {
return Collections.emptyList();
}
DocumentCallback<O> callback = new UnwrapAndReadDocumentCallback<O>(mongoConverter, outputType, collectionName);
List<O> mappedResults = new ArrayList<O>();
for (Document document : resultSet) {
mappedResults.add(callback.doWith(document));
}
return mappedResults;
}
protected <O> CloseableIterator<O> aggregateStream(Aggregation aggregation, String collectionName,
Class<O> outputType, AggregationOperationContext context) {
Assert.hasText(collectionName, "Collection name must not be null or empty!");
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
Assert.notNull(outputType, "Output type must not be null!");
AggregationOperationContext rootContext = context == null ? Aggregation.DEFAULT_CONTEXT : context;
Document command = aggregation.toDocument(collectionName, rootContext);
assertNotExplain(command);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Streaming aggregation: {}", serializeToJsonSafely(command));
}
ReadDocumentCallback<O> readCallback = new ReadDocumentCallback<O>(mongoConverter, outputType, collectionName);
return execute(collectionName, new CollectionCallback<CloseableIterator<O>>() {
@Override
public CloseableIterator<O> doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
List<Document> pipeline = (List<Document>) command.get("pipeline");
AggregationOptions options = AggregationOptions.fromDocument(command);
AggregateIterable<Document> cursor = collection.aggregate(pipeline).allowDiskUse(options.isAllowDiskUse())
.useCursor(true);
Integer cursorBatchSize = options.getCursorBatchSize();
if (cursorBatchSize != null) {
cursor = cursor.batchSize(cursorBatchSize);
}
if (options.getCollation().isPresent()) {
cursor = cursor.collation(options.getCollation().map(Collation::toMongoCollation).get());
}
return new CloseableIterableCursorAdapter<O>(cursor.iterator(), exceptionTranslator, readCallback);
}
});
}
/**
* Assert that the {@link Document} does not enable Aggregation explain mode.
*
* @param command the command {@link Document}.
*/
private void assertNotExplain(Document command) {
Boolean explain = command.get("explain", Boolean.class);
if (explain != null && explain) {
throw new IllegalArgumentException("Can't use explain option with streaming!");
}
}
protected String replaceWithResourceIfNecessary(String function) {
String func = function;
if (this.resourceLoader != null && ResourceUtils.isUrl(function)) {
Resource functionResource = resourceLoader.getResource(func);
if (!functionResource.exists()) {
throw new InvalidDataAccessApiUsageException(String.format("Resource %s not found!", function));
}
Scanner scanner = null;
try {
scanner = new Scanner(functionResource.getInputStream());
return scanner.useDelimiter("\\A").next();
} catch (IOException e) {
throw new InvalidDataAccessApiUsageException(String.format("Cannot read map-reduce file %s!", function), e);
} finally {
if (scanner != null) {
scanner.close();
}
}
}
return func;
}
public Set<String> getCollectionNames() {
return execute(new DbCallback<Set<String>>() {
public Set<String> doInDB(MongoDatabase db) throws MongoException, DataAccessException {
Set<String> result = new LinkedHashSet<String>();
for (String name : db.listCollectionNames()) {
result.add(name);
}
return result;
}
});
}
public MongoDatabase getDb() {
return mongoDbFactory.getDb();
}
protected <T> void maybeEmitEvent(MongoMappingEvent<T> event) {
if (null != eventPublisher) {
eventPublisher.publishEvent(event);
}
}
/**
* Create the specified collection using the provided options
*
* @param collectionName
* @param collectionOptions
* @return the collection that was created
*/
protected MongoCollection<Document> doCreateCollection(final String collectionName,
final Document collectionOptions) {
return execute(new DbCallback<MongoCollection<Document>>() {
public MongoCollection<Document> doInDB(MongoDatabase db) throws MongoException, DataAccessException {
CreateCollectionOptions co = new CreateCollectionOptions();
if (collectionOptions.containsKey("capped")) {
co.capped((Boolean) collectionOptions.get("capped"));
}
if (collectionOptions.containsKey("size")) {
co.sizeInBytes(((Number) collectionOptions.get("size")).longValue());
}
if (collectionOptions.containsKey("max")) {
co.maxDocuments(((Number) collectionOptions.get("max")).longValue());
}
if (collectionOptions.containsKey("collation")) {
co.collation(IndexConverters.fromDocument(collectionOptions.get("collation", Document.class)));
}
db.createCollection(collectionName, co);
MongoCollection<Document> coll = db.getCollection(collectionName, Document.class);
// TODO: Emit a collection created event
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Created collection [{}]", coll.getNamespace().getCollectionName());
}
return coll;
}
});
}
/**
* Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter.
* The query document is specified as a standard {@link Document} and so is the fields specification.
*
* @param collectionName name of the collection to retrieve the objects from.
* @param query the query document that specifies the criteria used to find a record.
* @param fields the document that specifies the fields to be returned.
* @param entityClass the parameterized type of the returned list.
* @return the {@link List} of converted objects.
*/
protected <T> T doFindOne(String collectionName, Document query, Document fields, Class<T> entityClass) {
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(entityClass);
Document mappedQuery = queryMapper.getMappedObject(query, entity);
Document mappedFields = fields == null ? null : queryMapper.getMappedObject(fields, entity);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("findOne using query: {} fields: {} for class: {} in collection: {}", serializeToJsonSafely(query),
mappedFields, entityClass, collectionName);
}
return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields),
new ReadDocumentCallback<T>(this.mongoConverter, entityClass, collectionName), collectionName);
}
/**
* Map the results of an ad-hoc query on the default MongoDB collection to a List using the template's converter. The
* query document is specified as a standard Document and so is the fields specification.
*
* @param collectionName name of the collection to retrieve the objects from
* @param query the query document that specifies the criteria used to find a record
* @param fields the document that specifies the fields to be returned
* @param entityClass the parameterized type of the returned list.
* @return the List of converted objects.
*/
protected <T> List<T> doFind(String collectionName, Document query, Document fields, Class<T> entityClass) {
return doFind(collectionName, query, fields, entityClass, null,
new ReadDocumentCallback<T>(this.mongoConverter, entityClass, collectionName));
}
/**
* Map the results of an ad-hoc query on the default MongoDB collection to a List of the specified type. The object is
* converted from the MongoDB native representation using an instance of {@see MongoConverter}. The query document is
* specified as a standard Document and so is the fields specification.
*
* @param collectionName name of the collection to retrieve the objects from.
* @param query the query document that specifies the criteria used to find a record.
* @param fields the document that specifies the fields to be returned.
* @param entityClass the parameterized type of the returned list.
* @param preparer allows for customization of the {@link DBCursor} used when iterating over the result set, (apply
* limits, skips and so on).
* @return the {@link List} of converted objects.
*/
protected <T> List<T> doFind(String collectionName, Document query, Document fields, Class<T> entityClass,
CursorPreparer preparer) {
return doFind(collectionName, query, fields, entityClass, preparer,
new ReadDocumentCallback<T>(mongoConverter, entityClass, collectionName));
}
protected <S, T> List<T> doFind(String collectionName, Document query, Document fields, Class<S> entityClass,
CursorPreparer preparer, DocumentCallback<T> objectCallback) {
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(entityClass);
Document mappedFields = queryMapper.getMappedFields(fields, entity);
Document mappedQuery = queryMapper.getMappedObject(query, entity);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("find using query: {} fields: {} for class: {} in collection: {}",
serializeToJsonSafely(mappedQuery), mappedFields, entityClass, collectionName);
}
return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer, objectCallback,
collectionName);
}
protected Document convertToDocument(CollectionOptions collectionOptions) {
Document document = new Document();
if (collectionOptions != null) {
if (collectionOptions.getCapped() != null) {
document.put("capped", collectionOptions.getCapped().booleanValue());
}
if (collectionOptions.getSize() != null) {
document.put("size", collectionOptions.getSize().intValue());
}
if (collectionOptions.getMaxDocuments() != null) {
document.put("max", collectionOptions.getMaxDocuments().intValue());
}
collectionOptions.getCollation().ifPresent(val -> document.append("collation", val.toDocument()));
}
return document;
}
/**
* Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter.
* The first document that matches the query is returned and also removed from the collection in the database.
* <p/>
* The query document is specified as a standard Document and so is the fields specification.
*
* @param collectionName name of the collection to retrieve the objects from
* @param query the query document that specifies the criteria used to find a record
* @param entityClass the parameterized type of the returned list.
* @return the List of converted objects.
*/
protected <T> T doFindAndRemove(String collectionName, Document query, Document fields, Document sort,
Collation collation, Class<T> entityClass) {
EntityReader<? super T, Bson> readerToUse = this.mongoConverter;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("findAndRemove using query: {} fields: {} sort: {} for class: {} in collection: {}",
serializeToJsonSafely(query), fields, sort, entityClass, collectionName);
}
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(entityClass);
return executeFindOneInternal(
new FindAndRemoveCallback(queryMapper.getMappedObject(query, entity), fields, sort, collation),
new ReadDocumentCallback<T>(readerToUse, entityClass, collectionName), collectionName);
}
protected <T> T doFindAndModify(String collectionName, Document query, Document fields, Document sort,
Class<T> entityClass, Update update, FindAndModifyOptions options) {
EntityReader<? super T, Bson> readerToUse = this.mongoConverter;
if (options == null) {
options = new FindAndModifyOptions();
}
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(entityClass);
increaseVersionForUpdateIfNecessary(entity, update);
Document mappedQuery = queryMapper.getMappedObject(query, entity);
Document mappedUpdate = updateMapper.getMappedObject(update.getUpdateObject(), entity);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"findAndModify using query: {} fields: {} sort: {} for class: {} and update: {} " + "in collection: {}",
serializeToJsonSafely(mappedQuery), fields, sort, entityClass, serializeToJsonSafely(mappedUpdate),
collectionName);
}
return executeFindOneInternal(new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, options),
new ReadDocumentCallback<T>(readerToUse, entityClass, collectionName), collectionName);
}
/**
* Populates the id property of the saved object, if it's not set already.
*
* @param savedObject
* @param id
*/
protected void populateIdIfNecessary(Object savedObject, Object id) {
if (id == null) {
return;
}
if (savedObject instanceof Document) {
Document document = (Document) savedObject;
document.put(ID_FIELD, id);
return;
}
getIdPropertyFor(savedObject.getClass()).ifPresent(idProp -> {
ConversionService conversionService = mongoConverter.getConversionService();
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(savedObject.getClass());
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(savedObject);
Optional<Object> value = accessor.getProperty(idProp);
if (!value.isPresent()) {
new ConvertingPropertyAccessor(accessor, conversionService).setProperty(idProp, Optional.of(id));
}
});
}
private MongoCollection<Document> getAndPrepareCollection(MongoDatabase db, String collectionName) {
try {
MongoCollection<Document> collection = db.getCollection(collectionName, Document.class);
collection = prepareCollection(collection);
return collection;
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
}
/**
* Internal method using callbacks to do queries against the datastore that requires reading a single object from a
* collection of objects. It will take the following steps
* <ol>
* <li>Execute the given {@link ConnectionCallback} for a {@link Document}.</li>
* <li>Apply the given {@link DocumentCallback} to each of the {@link Document}s to obtain the result.</li>
* <ol>
*
* @param <T>
* @param collectionCallback the callback to retrieve the {@link Document} with
* @param objectCallback the {@link DocumentCallback} to transform {@link Document}s into the actual domain type
* @param collectionName the collection to be queried
* @return
*/
private <T> T executeFindOneInternal(CollectionCallback<Document> collectionCallback,
DocumentCallback<T> objectCallback, String collectionName) {
try {
T result = objectCallback
.doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(), collectionName)));
return result;
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
}
/**
* Internal method using callback to do queries against the datastore that requires reading a collection of objects.
* It will take the following steps
* <ol>
* <li>Execute the given {@link ConnectionCallback} for a {@link DBCursor}.</li>
* <li>Prepare that {@link DBCursor} with the given {@link CursorPreparer} (will be skipped if {@link CursorPreparer}
* is {@literal null}</li>
* <li>Iterate over the {@link DBCursor} and applies the given {@link DocumentCallback} to each of the
* {@link Document}s collecting the actual result {@link List}.</li>
* <ol>
*
* @param <T>
* @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 DocumentCallback} to transform {@link Document}s into the actual domain type
* @param collectionName the collection to be queried
* @return
*/
private <T> List<T> executeFindMultiInternal(CollectionCallback<FindIterable<Document>> collectionCallback,
CursorPreparer preparer, DocumentCallback<T> objectCallback, String collectionName) {
try {
MongoCursor<Document> cursor = null;
try {
FindIterable<Document> iterable = collectionCallback
.doInCollection(getAndPrepareCollection(getDb(), collectionName));
if (preparer != null) {
iterable = preparer.prepare(iterable);
}
cursor = iterable.iterator();
List<T> result = new ArrayList<T>();
while (cursor.hasNext()) {
Document object = cursor.next();
result.add(objectCallback.doWith(object));
}
return result;
} finally {
if (cursor != null) {
cursor.close();
}
}
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
}
private void executeQueryInternal(CollectionCallback<FindIterable<Document>> collectionCallback,
CursorPreparer preparer, DocumentCallbackHandler callbackHandler, String collectionName) {
try {
MongoCursor<Document> cursor = null;
try {
FindIterable<Document> iterable = collectionCallback
.doInCollection(getAndPrepareCollection(getDb(), collectionName));
if (preparer != null) {
iterable = preparer.prepare(iterable);
}
cursor = iterable.iterator();
while (cursor.hasNext()) {
callbackHandler.processDocument(cursor.next());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
}
public PersistenceExceptionTranslator getExceptionTranslator() {
return exceptionTranslator;
}
private Optional<? extends MongoPersistentEntity<?>> getPersistentEntity(Class<?> type) {
return Optional.ofNullable(type).flatMap(it -> mappingContext.getPersistentEntity(it));
}
private Optional<MongoPersistentProperty> getIdPropertyFor(Class<?> type) {
return mappingContext.getPersistentEntity(type).flatMap(it -> it.getIdProperty());
}
private <T> String determineEntityCollectionName(T obj) {
if (null != obj) {
return determineCollectionName(obj.getClass());
}
return null;
}
String determineCollectionName(Class<?> entityClass) {
if (entityClass == null) {
throw new InvalidDataAccessApiUsageException(
"No class parameter provided, entity collection can't be determined!");
}
return mappingContext.getRequiredPersistentEntity(entityClass).getCollection();
}
private static final MongoConverter getDefaultMongoConverter(MongoDbFactory factory) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
converter.afterPropertiesSet();
return converter;
}
private Document getMappedSortObject(Query query, Class<?> type) {
if (query == null || query.getSortObject() == null) {
return null;
}
return queryMapper.getMappedSort(query.getSortObject(), mappingContext.getPersistentEntity(type));
}
/**
* Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original
* exception if the conversation failed. Thus allows safe re-throwing of the return value.
*
* @param ex the exception to translate
* @param exceptionTranslator the {@link PersistenceExceptionTranslator} to be used for translation
* @return
*/
static RuntimeException potentiallyConvertRuntimeException(RuntimeException ex,
PersistenceExceptionTranslator exceptionTranslator) {
RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex);
return resolved == null ? ex : resolved;
}
/**
* Returns all identifiers for the given documents. Will augment the given identifiers and fill in only the ones that
* are {@literal null} currently. This would've been better solved in {@link #insertDBObjectList(String, List)}
* directly but would require a signature change of that method.
*
* @param ids
* @param documents
* @return TODO: Remove for 2.0 and change method signature of {@link #insertDBObjectList(String, List)}.
*/
private static List<Object> consolidateIdentifiers(List<ObjectId> ids, List<Document> documents) {
List<Object> result = new ArrayList<Object>(ids.size());
for (int i = 0; i < ids.size(); i++) {
ObjectId objectId = ids.get(i);
result.add(objectId == null ? documents.get(i).get(ID_FIELD) : objectId);
}
return result;
}
// Callback implementations
/**
* Simple {@link CollectionCallback} that takes a query {@link Document} plus an optional fields specification
* {@link Document} and executes that against the {@link DBCollection}.
*
* @author Oliver Gierke
* @author Thomas Risberg
* @author Christoph Strobl
*/
private static class FindOneCallback implements CollectionCallback<Document> {
private final Document query;
private final Optional<Document> fields;
public FindOneCallback(Document query, Document fields) {
this.query = query;
this.fields = Optional.ofNullable(fields);
}
public Document doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
FindIterable<Document> iterable = collection.find(query);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query),
serializeToJsonSafely(fields.orElseGet(Document::new)), collection.getNamespace().getFullName());
}
if (fields.isPresent()) {
iterable = iterable.projection(fields.get());
}
return iterable.first();
}
}
/**
* Simple {@link CollectionCallback} that takes a query {@link Document} plus an optional fields specification
* {@link Document} and executes that against the {@link DBCollection}.
*
* @author Oliver Gierke
* @author Thomas Risberg
* @author Christoph Strobl
*/
private static class FindCallback implements CollectionCallback<FindIterable<Document>> {
private final Document query;
private final Optional<Document> fields;
public FindCallback(Document query) {
this(query, null);
}
public FindCallback(Document query, Document fields) {
this.query = query != null ? query : new Document();
this.fields = Optional.ofNullable(fields);
}
public FindIterable<Document> doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
FindIterable<Document> iterable = collection.find(query);
return fields.filter(val -> !val.isEmpty()).map(iterable::projection).orElse(iterable);
}
}
/**
* Simple {@link CollectionCallback} that takes a query {@link Document} plus an optional fields specification
* {@link Document} and executes that against the {@link DBCollection}.
*
* @author Thomas Risberg
*/
private static class FindAndRemoveCallback implements CollectionCallback<Document> {
private final Document query;
private final Document fields;
private final Document sort;
private final Optional<Collation> collation;
public FindAndRemoveCallback(Document query, Document fields, Document sort, Collation collation) {
this.query = query;
this.fields = fields;
this.sort = sort;
this.collation = Optional.ofNullable(collation);
}
public Document doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
FindOneAndDeleteOptions opts = new FindOneAndDeleteOptions().sort(sort).projection(fields);
collation.map(Collation::toMongoCollation).ifPresent(opts::collation);
return collection.findOneAndDelete(query, opts);
}
}
private static class FindAndModifyCallback implements CollectionCallback<Document> {
private final Document query;
private final Document fields;
private final Document sort;
private final Document update;
private final FindAndModifyOptions options;
public FindAndModifyCallback(Document query, Document fields, Document sort, Document update,
FindAndModifyOptions options) {
this.query = query;
this.fields = fields;
this.sort = sort;
this.update = update;
this.options = options;
}
public Document doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
FindOneAndUpdateOptions opts = new FindOneAndUpdateOptions();
opts.sort(sort);
if (options.isUpsert()) {
opts.upsert(true);
}
opts.projection(fields);
if (options.isReturnNew()) {
opts.returnDocument(ReturnDocument.AFTER);
}
options.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation);
return collection.findOneAndUpdate(query, update, opts);
}
}
/**
* Simple internal callback to allow operations on a {@link Document}.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
interface DocumentCallback<T> {
T doWith(Document object);
}
/**
* Simple {@link DocumentCallback} that will transform {@link Document} into the given target type using the given
* {@link MongoReader}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
private class ReadDocumentCallback<T> implements DocumentCallback<T> {
private final EntityReader<? super T, Bson> reader;
private final Class<T> type;
private final String collectionName;
public ReadDocumentCallback(EntityReader<? super T, Bson> reader, Class<T> type, String collectionName) {
Assert.notNull(reader, "EntityReader must not be null!");
Assert.notNull(type, "Entity type must not be null!");
this.reader = reader;
this.type = type;
this.collectionName = collectionName;
}
public T doWith(Document object) {
if (null != object) {
maybeEmitEvent(new AfterLoadEvent<T>(object, type, collectionName));
}
T source = reader.read(type, object);
if (null != source) {
maybeEmitEvent(new AfterConvertEvent<T>(object, source, collectionName));
}
return source;
}
}
class UnwrapAndReadDocumentCallback<T> extends ReadDocumentCallback<T> {
public UnwrapAndReadDocumentCallback(EntityReader<? super T, Bson> reader, Class<T> type, String collectionName) {
super(reader, type, collectionName);
}
@Override
public T doWith(Document object) {
Object idField = object.get(Fields.UNDERSCORE_ID);
if (!(idField instanceof Document)) {
return super.doWith(object);
}
Document toMap = new Document();
Document nested = (Document) idField;
toMap.putAll(nested);
for (String key : object.keySet()) {
if (!Fields.UNDERSCORE_ID.equals(key)) {
toMap.put(key, object.get(key));
}
}
return super.doWith(toMap);
}
}
class QueryCursorPreparer implements CursorPreparer {
private final Query query;
private final Class<?> type;
public QueryCursorPreparer(Query query, Class<?> type) {
this.query = query;
this.type = type;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.CursorPreparer#prepare(com.mongodb.DBCursor)
*/
public FindIterable<Document> prepare(FindIterable<Document> cursor) {
if (query == null) {
return cursor;
}
if (query.getSkip() <= 0 && query.getLimit() <= 0
&& (query.getSortObject() == null || query.getSortObject().isEmpty()) && !StringUtils.hasText(query.getHint())
&& !query.getMeta().hasValues() && !query.getCollation().isPresent()) {
return cursor;
}
FindIterable<Document> cursorToUse;
cursorToUse = query.getCollation().map(Collation::toMongoCollation).map(cursor::collation).orElse(cursor);
try {
if (query.getSkip() > 0) {
cursorToUse = cursorToUse.skip((int) query.getSkip());
}
if (query.getLimit() > 0) {
cursorToUse = cursorToUse.limit(query.getLimit());
}
if (query.getSortObject() != null && !query.getSortObject().isEmpty()) {
Document sort = type != null ? getMappedSortObject(query, type) : query.getSortObject();
cursorToUse = cursorToUse.sort(sort);
}
Document meta = new Document();
if (StringUtils.hasText(query.getHint())) {
meta.put("$hint", query.getHint());
}
if (query.getMeta().hasValues()) {
for (Entry<String, Object> entry : query.getMeta().values()) {
meta.put(entry.getKey(), entry.getValue());
}
for (Meta.CursorOption option : query.getMeta().getFlags()) {
switch (option) {
case NO_TIMEOUT:
cursorToUse = cursorToUse.noCursorTimeout(true);
break;
case PARTIAL:
cursorToUse = cursorToUse.partial(true);
break;
default:
throw new IllegalArgumentException(String.format("%s is no supported flag.", option));
}
}
}
cursorToUse = cursorToUse.modifiers(meta);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
return cursorToUse;
}
}
/**
* {@link DocumentCallback} that assumes a {@link GeoResult} to be created, delegates actual content unmarshalling to
* a delegate and creates a {@link GeoResult} from the result.
*
* @author Oliver Gierke
*/
static class GeoNearResultDocumentCallback<T> implements DocumentCallback<GeoResult<T>> {
private final DocumentCallback<T> delegate;
private final Metric metric;
/**
* Creates a new {@link GeoNearResultDocumentCallback} using the given {@link DocumentCallback} delegate for
* {@link GeoResult} content unmarshalling.
*
* @param delegate must not be {@literal null}.
*/
public GeoNearResultDocumentCallback(DocumentCallback<T> delegate, Metric metric) {
Assert.notNull(delegate, "DocumentCallback must not be null!");
this.delegate = delegate;
this.metric = metric;
}
public GeoResult<T> doWith(Document object) {
double distance = ((Double) object.get("dis")).doubleValue();
Document content = (Document) object.get("obj");
T doWith = delegate.doWith(content);
return new GeoResult<T>(doWith, new Distance(distance, metric));
}
}
/**
* A {@link CloseableIterator} that is backed by a MongoDB {@link Cursor}.
*
* @since 1.7
* @author Thomas Darimont
*/
static class CloseableIterableCursorAdapter<T> implements CloseableIterator<T> {
private volatile MongoCursor<Document> cursor;
private PersistenceExceptionTranslator exceptionTranslator;
private DocumentCallback<T> objectReadCallback;
CloseableIterableCursorAdapter(MongoCursor<Document> cursor, PersistenceExceptionTranslator exceptionTranslator,
DocumentCallback<T> objectReadCallback) {
this.cursor = cursor;
this.exceptionTranslator = exceptionTranslator;
this.objectReadCallback = objectReadCallback;
}
/**
* Creates a new {@link CloseableIterableCursorAdapter} backed by the given {@link Cursor}.
*
* @param cursor
* @param exceptionTranslator
* @param objectReadCallback
*/
public CloseableIterableCursorAdapter(FindIterable<Document> cursor,
PersistenceExceptionTranslator exceptionTranslator, DocumentCallback<T> objectReadCallback) {
this.cursor = cursor.iterator();
this.exceptionTranslator = exceptionTranslator;
this.objectReadCallback = objectReadCallback;
}
@Override
public boolean hasNext() {
if (cursor == null) {
return false;
}
try {
return cursor.hasNext();
} catch (RuntimeException ex) {
throw potentiallyConvertRuntimeException(ex, exceptionTranslator);
}
}
@Override
public T next() {
if (cursor == null) {
return null;
}
try {
Document item = cursor.next();
T converted = objectReadCallback.doWith(item);
return converted;
} catch (RuntimeException ex) {
throw potentiallyConvertRuntimeException(ex, exceptionTranslator);
}
}
@Override
public void close() {
MongoCursor<Document> c = cursor;
try {
c.close();
} catch (RuntimeException ex) {
throw potentiallyConvertRuntimeException(ex, exceptionTranslator);
} finally {
cursor = null;
exceptionTranslator = null;
objectReadCallback = null;
}
}
}
public MongoDbFactory getMongoDbFactory() {
return mongoDbFactory;
}
}