diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index bfcd871e6..4feef22e2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java @@ -120,19 +120,14 @@ public class DefaultIndexOperations implements IndexOperations { return execute(collection -> { - Document indexOptions = indexDefinition.getIndexOptions(); + MongoPersistentEntity entity = lookupPersistentEntity(type, collectionName); - IndexOptions ops = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); + IndexOptions indexOptions = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); - if (indexOptions.containsKey(PARTIAL_FILTER_EXPRESSION_KEY)) { + indexOptions = addPartialFilterIfPresent(indexOptions, indexDefinition.getIndexOptions(), entity); + indexOptions = addDefaultCollationIfRequired(indexOptions, entity); - Assert.isInstanceOf(Document.class, indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY)); - - ops.partialFilterExpression(mapper.getMappedObject((Document) indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), - lookupPersistentEntity(type, collectionName))); - } - - return collection.createIndex(indexDefinition.getIndexKeys(), ops); + return collection.createIndex(indexDefinition.getIndexKeys(), indexOptions); }); } @@ -192,7 +187,7 @@ public class DefaultIndexOperations implements IndexOperations { private List getIndexData(MongoCursor cursor) { - List indexInfoList = new ArrayList(); + List indexInfoList = new ArrayList<>(); while (cursor.hasNext()) { @@ -217,4 +212,25 @@ public class DefaultIndexOperations implements IndexOperations { return mongoOperations.execute(collectionName, callback); } + + private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document sourceOptions, + @Nullable MongoPersistentEntity entity) { + + if (!sourceOptions.containsKey(PARTIAL_FILTER_EXPRESSION_KEY)) { + return ops; + } + + Assert.isInstanceOf(Document.class, sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY)); + return ops.partialFilterExpression( + mapper.getMappedObject((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity)); + } + + private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops, MongoPersistentEntity entity) { + + if (ops.getCollation() != null || entity == null || !entity.hasCollation()) { + return ops; + } + + return ops.collation(entity.getCollation().toMongoCollation()); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java index 0e43f94ea..dbfc2a3b5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java @@ -94,23 +94,16 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { return mongoOperations.execute(collectionName, collection -> { - Document indexOptions = indexDefinition.getIndexOptions(); + MongoPersistentEntity entity = type + .map(val -> (MongoPersistentEntity) queryMapper.getMappingContext().getRequiredPersistentEntity(val)) + .orElseGet(() -> lookupPersistentEntity(collectionName)); - IndexOptions ops = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); + IndexOptions indexOptions = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); - if (indexOptions.containsKey(PARTIAL_FILTER_EXPRESSION_KEY)) { + indexOptions = addPartialFilterIfPresent(indexOptions, indexDefinition.getIndexOptions(), entity); + indexOptions = addDefaultCollationIfRequired(indexOptions, entity); - Assert.isInstanceOf(Document.class, indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY)); - - MongoPersistentEntity entity = type - .map(val -> (MongoPersistentEntity) queryMapper.getMappingContext().getRequiredPersistentEntity(val)) - .orElseGet(() -> lookupPersistentEntity(collectionName)); - - ops = ops.partialFilterExpression( - queryMapper.getMappedObject(indexOptions.get(PARTIAL_FILTER_EXPRESSION_KEY, Document.class), entity)); - } - - return collection.createIndex(indexDefinition.getIndexKeys(), ops); + return collection.createIndex(indexDefinition.getIndexKeys(), indexOptions); }).next(); } @@ -126,21 +119,24 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { .orElse(null); } - /* (non-Javadoc) + /* + * (non-Javadoc) * @see org.springframework.data.mongodb.core.index.ReactiveIndexOperations#dropIndex(java.lang.String) */ public Mono dropIndex(final String name) { return mongoOperations.execute(collectionName, collection -> collection.dropIndex(name)).then(); } - /* (non-Javadoc) + /* + * (non-Javadoc) * @see org.springframework.data.mongodb.core.index.ReactiveIndexOperations#dropAllIndexes() */ public Mono dropAllIndexes() { return dropIndex("*"); } - /* (non-Javadoc) + /* + * (non-Javadoc) * @see org.springframework.data.mongodb.core.index.ReactiveIndexOperations#getIndexInfo() */ public Flux getIndexInfo() { @@ -148,4 +144,25 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { return mongoOperations.execute(collectionName, collection -> collection.listIndexes(Document.class)) // .map(IndexConverters.documentToIndexInfoConverter()::convert); } + + private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document sourceOptions, + @Nullable MongoPersistentEntity entity) { + + if (!sourceOptions.containsKey(PARTIAL_FILTER_EXPRESSION_KEY)) { + return ops; + } + + Assert.isInstanceOf(Document.class, sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY)); + return ops.partialFilterExpression( + queryMapper.getMappedObject((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity)); + } + + private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops, MongoPersistentEntity entity) { + + if (ops.getCollation() != null || entity == null || !entity.hasCollation()) { + return ops; + } + + return ops.collation(entity.getCollation().toMongoCollation()); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java index 62452a51e..0c18a38a1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java @@ -33,6 +33,31 @@ public class FindAndModifyOptions { private @Nullable Collation collation; + private static final FindAndModifyOptions NONE = new FindAndModifyOptions() { + + private static final String ERROR_MSG = "FindAndModifyOptions.none() cannot be changed. Please use FindAndModifyOptions.options() instead."; + + @Override + public FindAndModifyOptions returnNew(boolean returnNew) { + throw new UnsupportedOperationException(ERROR_MSG); + } + + @Override + public FindAndModifyOptions upsert(boolean upsert) { + throw new UnsupportedOperationException(ERROR_MSG); + } + + @Override + public FindAndModifyOptions remove(boolean remove) { + throw new UnsupportedOperationException(ERROR_MSG); + } + + @Override + public FindAndModifyOptions collation(Collation collation) { + throw new UnsupportedOperationException(ERROR_MSG); + } + }; + /** * Static factory method to create a FindAndModifyOptions instance * @@ -42,6 +67,16 @@ public class FindAndModifyOptions { return new FindAndModifyOptions(); } + /** + * Static factory method returning an unmodifiable {@link FindAndModifyOptions} instance. + * + * @return unmodifiable {@link FindAndModifyOptions} instance. + * @since 2.2 + */ + public static FindAndModifyOptions none() { + return NONE; + } + /** * Create new {@link FindAndModifyOptions} based on option of given {@litearl source}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java index 97c840af8..1da434326 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java @@ -36,6 +36,21 @@ public class FindAndReplaceOptions { private boolean returnNew; private boolean upsert; + private static final FindAndReplaceOptions NONE = new FindAndReplaceOptions() { + + private static final String ERROR_MSG = "FindAndReplaceOptions.none() cannot be changed. Please use FindAndReplaceOptions.options() instead."; + + @Override + public FindAndReplaceOptions returnNew() { + throw new UnsupportedOperationException(ERROR_MSG); + } + + @Override + public FindAndReplaceOptions upsert() { + throw new UnsupportedOperationException(ERROR_MSG); + } + }; + /** * Static factory method to create a {@link FindAndReplaceOptions} instance. *
@@ -51,6 +66,16 @@ public class FindAndReplaceOptions { return new FindAndReplaceOptions(); } + /** + * Static factory method returning an unmodifiable {@link FindAndReplaceOptions} instance. + * + * @return unmodifiable {@link FindAndReplaceOptions} instance. + * @since 2.2 + */ + public static FindAndReplaceOptions none() { + return NONE; + } + /** * Static factory method to create a {@link FindAndReplaceOptions} instance with *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 53e268777..48572b074 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -515,7 +515,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, sortObject, fieldsObject, collectionName); } - this.executeQueryInternal(new FindCallback(queryObject, fieldsObject), preparer, documentCallbackHandler, + this.executeQueryInternal(new FindCallback(queryObject, fieldsObject, null), preparer, documentCallbackHandler, collectionName); } @@ -602,7 +602,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.MongoOperations#createCollection(java.lang.Class) */ public MongoCollection createCollection(Class entityClass) { - return createCollection(operations.determineCollectionName(entityClass)); + return createCollection(entityClass, CollectionOptions.empty()); } /* @@ -613,8 +613,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable CollectionOptions collectionOptions) { Assert.notNull(entityClass, "EntityClass must not be null!"); - return doCreateCollection(operations.determineCollectionName(entityClass), - convertToDocument(collectionOptions, entityClass)); + + CollectionOptions options = collectionOptions != null ? collectionOptions : CollectionOptions.empty(); + options = Optionals + .firstNonEmpty(() -> collectionOptions != null ? collectionOptions.getCollation() : Optional.empty(), + () -> getCollationForType(entityClass)) // + .map(options::collation).orElse(options); + + return doCreateCollection(operations.determineCollectionName(entityClass), convertToDocument(options, entityClass)); } /* @@ -786,8 +792,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(entityClass, "EntityClass must not be null!"); Assert.notNull(collectionName, "CollectionName must not be null!"); - if (ObjectUtils.isEmpty(query.getSortObject()) && !query.getCollation().isPresent()) { - return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass); + if (ObjectUtils.isEmpty(query.getSortObject())) { + + return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) + .map(Collation::toMongoCollation).orElse(null), + entityClass); } else { query.limit(1); List results = find(query, entityClass, collectionName); @@ -816,7 +826,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), getPersistentEntity(entityClass)); return execute(collectionName, - new ExistsCallback(mappedQuery, query.getCollation().map(Collation::toMongoCollation).orElse(null))); + new ExistsCallback(mappedQuery, + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) + .map(Collation::toMongoCollation).orElse(null))); } // Find methods that take a Query to express the query and that return a List of objects. @@ -906,7 +918,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, DistinctIterable iterable = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType); - return query.getCollation().map(Collation::toMongoCollation).map(iterable::collation).orElse(iterable); + return Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) + .map(Collation::toMongoCollation) // + .map(iterable::collation) // + .orElse(iterable); }); if (resultClass == Object.class || mongoDriverCompatibleType != resultClass) { @@ -1063,7 +1078,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, "Both Query and FindAndModifyOptions define a collation. Please provide the collation only via one of the two."); }); - query.getCollation().ifPresent(optionsToUse::collation); + Optionals + .firstNonEmpty(() -> query.getCollation(), () -> options.getCollation(), () -> getCollationForType(entityClass)) + .ifPresent(optionsToUse::collation); return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); @@ -1096,8 +1113,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument(); return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, - query.getCollation().map(Collation::toMongoCollation).orElse(null), entityType, mappedReplacement, options, - resultType); + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityType)) + .map(Collation::toMongoCollation).orElse(null), + entityType, mappedReplacement, options, resultType); } // Find methods that take a Query to express the query and that return a single object that is also removed from the @@ -1118,7 +1136,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(collectionName, "CollectionName must not be null!"); return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(), - getMappedSortObject(query, entityClass), query.getCollation().orElse(null), entityClass); + getMappedSortObject(query, entityClass), + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)).orElse(null), + entityClass); } @Override @@ -1610,11 +1630,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document queryObj = new Document(); if (query != null) { - queryObj.putAll(queryMapper.getMappedObject(query.getQueryObject(), entity)); - query.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation); } + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) // + .map(Collation::toMongoCollation) // + .ifPresent(opts::collation); + Document updateObj = update instanceof MappedUpdate ? update.getUpdateObject() : updateMapper.getMappedObject(update.getUpdateObject(), entity); @@ -1717,7 +1739,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document removeQuery = queryObject; DeleteOptions options = new DeleteOptions(); - query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation); + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) // + .map(Collation::toMongoCollation) // + .ifPresent(options::collation); MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName, entityClass, null, queryObject); @@ -1764,8 +1788,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public List findAll(Class entityClass, String collectionName) { - return executeFindMultiInternal(new FindCallback(new Document(), new Document()), null, - new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); + return executeFindMultiInternal( + new FindCallback(new Document(), new Document(), + getCollationForType(entityClass).map(Collation::toMongoCollation).orElse(null)), + null, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); } @Override @@ -1879,6 +1905,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } } + if(!collation.isPresent()) { + collation = getCollationForType(domainType); + } + mapReduce = collation.map(Collation::toMongoCollation).map(mapReduce::collation).orElse(mapReduce); List mappedResults = new ArrayList<>(); @@ -2123,8 +2153,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, List rawResult = new ArrayList<>(); + Optional collation = Optionals.firstNonEmpty(() -> options.getCollation(), () -> getCollationForType( + aggregation instanceof TypedAggregation ? ((TypedAggregation) aggregation).getInputType() : null)); + AggregateIterable aggregateIterable = collection.aggregate(pipeline, Document.class) // - .collation(options.getCollation().map(Collation::toMongoCollation).orElse(null)) // + .collation(collation.map(Collation::toMongoCollation).orElse(null)) // .allowDiskUse(options.isAllowDiskUse()); if (options.getCursorBatchSize() != null) { @@ -2165,16 +2198,18 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return execute(collectionName, (CollectionCallback>) collection -> { AggregateIterable cursor = collection.aggregate(pipeline, Document.class) // - .allowDiskUse(options.isAllowDiskUse()) // - .useCursor(true); + .allowDiskUse(options.isAllowDiskUse()); if (options.getCursorBatchSize() != null) { cursor = cursor.batchSize(options.getCursorBatchSize()); } - if (options.getCollation().isPresent()) { - cursor = cursor.collation(options.getCollation().map(Collation::toMongoCollation).get()); - } + Optionals + .firstNonEmpty(() -> options.getCollation(), + () -> getCollationForType( + aggregation instanceof TypedAggregation ? ((TypedAggregation) aggregation).getInputType() : null)) // + .map(Collation::toMongoCollation) // + .ifPresent(cursor::collation); return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, readCallback); }); @@ -2368,6 +2403,21 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @return the {@link List} of converted objects. */ protected T doFindOne(String collectionName, Document query, Document fields, Class entityClass) { + return doFindOne(collectionName, query, fields, null, entityClass); + } + + /** + * 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 doFindOne(String collectionName, Document query, Document fields, + @Nullable com.mongodb.client.model.Collation collation, Class entityClass) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); Document mappedQuery = queryMapper.getMappedObject(query, entity); @@ -2378,7 +2428,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, mappedFields, entityClass, collectionName); } - return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields), + return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, collation), new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); } @@ -2429,7 +2479,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, serializeToJsonSafely(mappedQuery), mappedFields, entityClass, collectionName); } - return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer, objectCallback, + return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), preparer, objectCallback, collectionName); } @@ -2452,7 +2502,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, serializeToJsonSafely(mappedQuery), mappedFields, sourceClass, collectionName); } - return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer, + return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), preparer, new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName); } @@ -2819,15 +2869,21 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final Document query; private final Optional fields; + private final @Nullable com.mongodb.client.model.Collation collation; + + public FindOneCallback(Document query, Document fields, @Nullable com.mongodb.client.model.Collation collation) { - public FindOneCallback(Document query, Document fields) { this.query = query; this.fields = Optional.of(fields).filter(it -> !ObjectUtils.isEmpty(fields)); + this.collation = collation; } public Document doInCollection(MongoCollection collection) throws MongoException, DataAccessException { FindIterable iterable = collection.find(query, Document.class); + if (collation != null) { + iterable = iterable.collation(collation); + } if (LOGGER.isDebugEnabled()) { @@ -2856,20 +2912,27 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final Document query; private final Document fields; + private final @Nullable com.mongodb.client.model.Collation collation; - public FindCallback(Document query, Document fields) { + public FindCallback(Document query, Document fields, @Nullable com.mongodb.client.model.Collation collation) { Assert.notNull(query, "Query must not be null!"); Assert.notNull(fields, "Fields must not be null!"); this.query = query; this.fields = fields; + this.collation = collation; } public FindIterable doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - return collection.find(query, Document.class).projection(fields); + FindIterable findIterable = collection.find(query, Document.class).projection(fields); + + if (collation != null) { + findIterable = findIterable.collation(collation); + } + return findIterable; } } @@ -3156,20 +3219,23 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ public FindIterable prepare(FindIterable cursor) { + FindIterable cursorToUse = cursor; + + Optionals + .firstNonEmpty(() -> query != null ? query.getCollation() : Optional.empty(), () -> getCollationForType(type)) + .map(Collation::toMongoCollation) // + .ifPresent(cursorToUse::collation); + if (query == null) { - return cursor; + return cursorToUse; } Meta meta = query.getMeta(); if (query.getSkip() <= 0 && query.getLimit() <= 0 && ObjectUtils.isEmpty(query.getSortObject()) && !StringUtils.hasText(query.getHint()) && !meta.hasValues() && !query.getCollation().isPresent()) { - return cursor; + return cursorToUse; } - FindIterable cursorToUse; - - cursorToUse = query.getCollation().map(Collation::toMongoCollation).map(cursor::collation).orElse(cursor); - try { if (query.getSkip() > 0) { cursorToUse = cursorToUse.skip((int) query.getSkip()); @@ -3417,4 +3483,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return execute(collectionName, collection -> collection.countDocuments(filter, options)); } } + + @Nullable + private Optional getCollationForType(Class type) { + + if (type == null || type == Document.class) { + return Optional.empty(); + } + MongoPersistentEntity entity = mappingContext.getPersistentEntity(type); + return entity != null ? Optional.ofNullable(entity.getCollation()) : Optional.empty(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 56fc38336..0ce065269 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -40,7 +40,6 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -70,7 +69,16 @@ import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.PrefixingDelegatingAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; -import org.springframework.data.mongodb.core.convert.*; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.JsonSchemaMapper; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; +import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +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.ReactiveIndexOperations; import org.springframework.data.mongodb.core.index.ReactiveMongoPersistentEntityIndexCreator; @@ -113,11 +121,29 @@ import com.mongodb.Mongo; import com.mongodb.MongoException; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; -import com.mongodb.client.model.*; +import com.mongodb.client.model.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.DeleteOptions; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndReplaceOptions; +import com.mongodb.client.model.FindOneAndUpdateOptions; +import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.client.model.ReturnDocument; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.ValidationOptions; import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; -import com.mongodb.reactivestreams.client.*; +import com.mongodb.reactivestreams.client.AggregatePublisher; +import com.mongodb.reactivestreams.client.ChangeStreamPublisher; +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.DistinctPublisher; +import com.mongodb.reactivestreams.client.FindPublisher; +import com.mongodb.reactivestreams.client.MapReducePublisher; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import com.mongodb.reactivestreams.client.Success; /** * Primary implementation of {@link ReactiveMongoOperations}. It simplifies the use of Reactive MongoDB usage and helps @@ -609,7 +635,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#createCollection(java.lang.Class) */ public Mono> createCollection(Class entityClass) { - return createCollection(determineCollectionName(entityClass)); + return createCollection(entityClass, CollectionOptions.empty()); } /* @@ -618,8 +644,17 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati */ public Mono> createCollection(Class entityClass, @Nullable CollectionOptions collectionOptions) { + + Assert.notNull(entityClass, "EntityClass must not be null!"); + + CollectionOptions options = collectionOptions != null ? collectionOptions : CollectionOptions.empty(); + options = Optionals + .firstNonEmpty(() -> collectionOptions != null ? collectionOptions.getCollation() : Optional.empty(), + () -> getCollationForType(entityClass)) // + .map(options::collation).orElse(options); + return doCreateCollection(determineCollectionName(entityClass), - convertToCreateCollectionOptions(collectionOptions, entityClass)); + convertToCreateCollectionOptions(options, entityClass)); } /* @@ -719,7 +754,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati if (ObjectUtils.isEmpty(query.getSortObject())) { return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass, - query.getCollation().orElse(null)); + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)).orElse(null)); } query.limit(1); @@ -762,8 +797,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati LOGGER.debug("exists: {} in collection: {}", serializeToJsonSafely(filter), collectionName); } - findPublisher = query.getCollation().map(Collation::toMongoCollation).map(findPublisher::collation) - .orElse(findPublisher); + findPublisher = Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) + .map(Collation::toMongoCollation).map(findPublisher::collation).orElse(findPublisher); return findPublisher.limit(1); }).hasElements(); @@ -849,8 +884,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } DistinctPublisher publisher = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType); - - return query.getCollation().map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher); + return Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) + .map(Collation::toMongoCollation) // + .map(publisher::collation) // + .orElse(publisher); }); if (resultClass == Object.class || mongoDriverCompatibleType != resultClass) { @@ -960,11 +997,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } ReadDocumentCallback readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); - return execute(collectionName, collection -> aggregateAndMap(collection, pipeline, options, readCallback)); + return execute(collectionName, collection -> aggregateAndMap(collection, pipeline, options, readCallback, + aggregation instanceof TypedAggregation ? ((TypedAggregation) aggregation).getInputType() : null)); } private Flux aggregateAndMap(MongoCollection collection, List pipeline, - AggregationOptions options, ReadDocumentCallback readCallback) { + AggregationOptions options, ReadDocumentCallback readCallback, @Nullable Class inputType) { AggregatePublisher cursor = collection.aggregate(pipeline, Document.class) .allowDiskUse(options.isAllowDiskUse()); @@ -973,9 +1011,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati cursor = cursor.batchSize(options.getCursorBatchSize()); } - if (options.getCollation().isPresent()) { - cursor = cursor.collation(options.getCollation().map(Collation::toMongoCollation).get()); - } + Optionals.firstNonEmpty(() -> options.getCollation(), () -> getCollationForType(inputType)) // + .map(Collation::toMongoCollation) // + .ifPresent(cursor::collation); return Flux.from(cursor).map(readCallback::doWith); } @@ -1072,6 +1110,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati public Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass, String collectionName) { + Assert.notNull(options, "Options must not be null! "); + FindAndModifyOptions optionsToUse = FindAndModifyOptions.of(options); Optionals.ifAllPresent(query.getCollation(), optionsToUse.getCollation(), (l, r) -> { @@ -1079,7 +1119,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati "Both Query and FindAndModifyOptions define a collation. Please provide the collation only via one of the two."); }); - query.getCollation().ifPresent(optionsToUse::collation); + Optionals + .firstNonEmpty(() -> query.getCollation(), () -> options.getCollation(), () -> getCollationForType(entityClass)) + .ifPresent(optionsToUse::collation); return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); @@ -1112,8 +1154,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument(); return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, - query.getCollation().map(Collation::toMongoCollation).orElse(null), entityType, mappedReplacement, options, - resultType); + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityType)) + .map(Collation::toMongoCollation).orElse(null), + entityType, mappedReplacement, options, resultType); } /* @@ -1131,7 +1174,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati public Mono findAndRemove(Query query, Class entityClass, String collectionName) { return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(), - getMappedSortObject(query, entityClass), query.getCollation().orElse(null), entityClass); + getMappedSortObject(query, entityClass), + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)).orElse(null), + entityClass); } /* @@ -1180,6 +1225,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } } + Optionals + .firstNonEmpty(() -> query != null ? query.getCollation() : Optional.empty(), + () -> getCollationForType(entityClass)) + .map(Collation::toMongoCollation) // + .ifPresent(options::collation); + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Executing count: {} in collection: {}", serializeToJsonSafely(filter), collectionName); } @@ -1647,7 +1698,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati MongoCollection collectionToUse = prepareCollection(collection, writeConcernToUse); UpdateOptions updateOptions = new UpdateOptions().upsert(upsert); - query.getCollation().map(Collation::toMongoCollation).ifPresent(updateOptions::collation); + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) // + .map(Collation::toMongoCollation) // + .ifPresent(updateOptions::collation); if (update.hasArrayFilters()) { updateOptions.arrayFilters(update.getArrayFilters().stream().map(ArrayFilter::asDocument) @@ -1811,7 +1864,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati null, removeQuery); DeleteOptions deleteOptions = new DeleteOptions(); - query.getCollation().map(Collation::toMongoCollation).ifPresent(deleteOptions::collation); + + Optionals.firstNonEmpty(() -> query.getCollation(), () -> getCollationForType(entityClass)) // + .map(Collation::toMongoCollation) // + .ifPresent(deleteOptions::collation); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); MongoCollection collectionToUse = prepareCollection(collection, writeConcernToUse); @@ -2381,7 +2437,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati collectionName)); } - return executeFindOneInternal(new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), + return executeFindOneInternal( + new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, + update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); }); } @@ -2781,12 +2839,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return collection.findOneAndDelete(query, findOneAndDeleteOptions); } - FindOneAndUpdateOptions findOneAndUpdateOptions = convertToFindOneAndUpdateOptions(options, fields, sort, arrayFilters); + FindOneAndUpdateOptions findOneAndUpdateOptions = convertToFindOneAndUpdateOptions(options, fields, sort, + arrayFilters); return collection.findOneAndUpdate(query, update, findOneAndUpdateOptions); } - private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options, Document fields, - Document sort, List arrayFilters) { + private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options, + Document fields, Document sort, List arrayFilters) { FindOneAndUpdateOptions result = new FindOneAndUpdateOptions(); @@ -3023,15 +3082,16 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @SuppressWarnings("deprecation") public FindPublisher prepare(FindPublisher findPublisher) { + FindPublisher findPublisherToUse = Optionals // + .firstNonEmpty(() -> query != null ? query.getCollation() : Optional.empty(), () -> getCollationForType(type)) + .map(Collation::toMongoCollation) // + .map(findPublisher::collation) // + .orElse(findPublisher); + if (query == null) { - return findPublisher; + return findPublisherToUse; } - FindPublisher findPublisherToUse; - - findPublisherToUse = query.getCollation().map(Collation::toMongoCollation).map(findPublisher::collation) - .orElse(findPublisher); - Meta meta = query.getMeta(); if (query.getSkip() <= 0 && query.getLimit() <= 0 && ObjectUtils.isEmpty(query.getSortObject()) && !StringUtils.hasText(query.getHint()) && !meta.hasValues()) { @@ -3205,4 +3265,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } } } + + @Nullable + private Optional getCollationForType(Class type) { + + if (type == null) { + return Optional.empty(); + } + MongoPersistentEntity entity = mappingContext.getPersistentEntity(type); + return entity != null ? Optional.ofNullable(entity.getCollation()) : Optional.empty(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java index c4b507e38..96f09e079 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java @@ -123,7 +123,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { String collectionName = getCollectionName(); - return template.findAndModify(query, update, findAndModifyOptions, targetType, collectionName); + return template.findAndModify(query, update, findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), targetType, collectionName); } /* @@ -133,7 +133,7 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation { @Override public Mono findAndReplace() { return template.findAndReplace(query, replacement, - findAndReplaceOptions != null ? findAndReplaceOptions : new FindAndReplaceOptions(), (Class) domainType, + findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.none(), (Class) domainType, getCollectionName(), targetType); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java index 757909d26..3f4109039 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java @@ -60,6 +60,9 @@ public class BasicMongoPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity extends PersistentEntity extends PersistentEntity findAllByDynamicSpElCollation(String collation); * * + * @return an empty {@link String} by default. * @since 2.2 */ String collation() default ""; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java index 51fa02479..22242a4f9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.repository.core.EntityInformation; import org.springframework.lang.Nullable; @@ -61,4 +62,23 @@ public interface MongoEntityInformation extends EntityInformation default Object getVersion(T entity) { return null; } + + /** + * Returns whether the entity defines a specific collation. + * + * @return {@literal true} if the entity defines a collation. + * @since 2.2 + */ + default boolean hasCollation() { + return getCollation() != null; + } + + /** + * Return the collation for the entity or {@literal null} if {@link #hasCollation() not defined}. + * + * @return can be {@literal null}. + * @since 2.2 + */ + @Nullable + Collation getCollation(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java index 594cd9487..9f81af2e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.repository.support; import org.bson.types.ObjectId; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.repository.core.support.PersistentEntityInformation; import org.springframework.lang.Nullable; @@ -143,4 +144,13 @@ public class MappingMongoEntityInformation extends PersistentEntityInform return accessor.getProperty(this.entityMetadata.getRequiredVersionProperty()); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.MongoEntityInformation#getCollation() + */ + @Nullable + public Collation getCollation() { + return this.entityMetadata.getCollation(); + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index 678a7a0c2..2ad380c78 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -227,7 +227,7 @@ public class SimpleMongoRepository implements MongoRepository { Long count = count(); List list = findAll(new Query().with(pageable)); - return new PageImpl(list, pageable, count); + return new PageImpl<>(list, pageable, count); } /* @@ -282,7 +282,9 @@ public class SimpleMongoRepository implements MongoRepository { Assert.notNull(example, "Sample must not be null!"); Assert.notNull(pageable, "Pageable must not be null!"); - Query query = new Query(new Criteria().alike(example)).with(pageable); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()).with(pageable); // + List list = mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName()); return PageableExecutionUtils.getPage(list, pageable, @@ -299,9 +301,11 @@ public class SimpleMongoRepository implements MongoRepository { Assert.notNull(example, "Sample must not be null!"); Assert.notNull(sort, "Sort must not be null!"); - Query q = new Query(new Criteria().alike(example)).with(sort); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()) // + .with(sort); - return mongoOperations.find(q, example.getProbeType(), entityInformation.getCollectionName()); + return mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName()); } /* @@ -322,9 +326,11 @@ public class SimpleMongoRepository implements MongoRepository { Assert.notNull(example, "Sample must not be null!"); - Query q = new Query(new Criteria().alike(example)); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()); + return Optional - .ofNullable(mongoOperations.findOne(q, example.getProbeType(), entityInformation.getCollectionName())); + .ofNullable(mongoOperations.findOne(query, example.getProbeType(), entityInformation.getCollectionName())); } /* @@ -336,8 +342,10 @@ public class SimpleMongoRepository implements MongoRepository { Assert.notNull(example, "Sample must not be null!"); - Query q = new Query(new Criteria().alike(example)); - return mongoOperations.count(q, example.getProbeType(), entityInformation.getCollectionName()); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()); + + return mongoOperations.count(query, example.getProbeType(), entityInformation.getCollectionName()); } /* @@ -349,8 +357,10 @@ public class SimpleMongoRepository implements MongoRepository { Assert.notNull(example, "Sample must not be null!"); - Query q = new Query(new Criteria().alike(example)); - return mongoOperations.exists(q, example.getProbeType(), entityInformation.getCollectionName()); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()); + + return mongoOperations.exists(query, example.getProbeType(), entityInformation.getCollectionName()); } private Query getIdQuery(Object id) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java index 3bb7025f6..eec5c2c15 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java @@ -91,10 +91,11 @@ public class SimpleReactiveMongoRepository implement Assert.notNull(example, "Sample must not be null!"); - Query q = new Query(new Criteria().alike(example)); - q.limit(2); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()) // + .limit(2); - return mongoOperations.find(q, example.getProbeType(), entityInformation.getCollectionName()).buffer(2) + return mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName()).buffer(2) .map(vals -> { if (vals.size() > 1) { @@ -140,8 +141,10 @@ public class SimpleReactiveMongoRepository implement Assert.notNull(example, "Sample must not be null!"); - Query q = new Query(new Criteria().alike(example)); - return mongoOperations.exists(q, example.getProbeType(), entityInformation.getCollectionName()); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()); + + return mongoOperations.exists(query, example.getProbeType(), entityInformation.getCollectionName()); } /* @@ -200,7 +203,9 @@ public class SimpleReactiveMongoRepository implement Assert.notNull(example, "Sample must not be null!"); Assert.notNull(sort, "Sort must not be null!"); - Query query = new Query(new Criteria().alike(example)).with(sort); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()) // + .with(sort); return mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName()); } @@ -235,8 +240,10 @@ public class SimpleReactiveMongoRepository implement Assert.notNull(example, "Sample must not be null!"); - Query q = new Query(new Criteria().alike(example)); - return mongoOperations.count(q, example.getProbeType(), entityInformation.getCollectionName()); + Query query = new Query(new Criteria().alike(example)) // + .collation(entityInformation.getCollation()); + + return mongoOperations.count(query, example.getProbeType(), entityInformation.getCollectionName()); } /* diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsUnitTests.java new file mode 100644 index 000000000..01f696dad --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsUnitTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2019 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 + * + * https://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.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import lombok.Data; + +import org.bson.Document; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.index.Index; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.query.Collation; + +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.IndexOptions; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultIndexOperationsUnitTests { + + MongoTemplate template; + + @Mock MongoDbFactory factory; + @Mock MongoDatabase db; + @Mock MongoCollection collection; + + MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); + MappingMongoConverter converter; + MongoMappingContext mappingContext; + + @Before + public void setUp() { + + when(factory.getDb()).thenReturn(db); + when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator); + when(db.getCollection(any(), any(Class.class))).thenReturn(collection); + when(collection.createIndex(any(), any(IndexOptions.class))).thenReturn("OK"); + + this.mappingContext = new MongoMappingContext(); + this.converter = spy(new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext)); + this.template = new MongoTemplate(factory, converter); + } + + @Test // DATAMONGO-1854 + public void ensureIndexDoesNotSetCollectionIfNoDefaultDefined() { + + indexOpsFor(Jedi.class).ensureIndex(new Index("firstname", Direction.DESC)); + + ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); + verify(collection).createIndex(any(), options.capture()); + + assertThat(options.getValue().getCollation()).isNull(); + } + + @Test // DATAMONGO-1854 + public void ensureIndexUsesDefaultCollationIfNoneDefinedInOptions() { + + indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC)); + + ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); + verify(collection).createIndex(any(), options.capture()); + + assertThat(options.getValue().getCollation()) + .isEqualTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build()); + } + + @Test // DATAMONGO-1854 + public void ensureIndexDoesNotUseDefaultCollationIfExplicitlySpecifiedInTheIndex() { + + indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC).collation(Collation.of("en_US"))); + + ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); + verify(collection).createIndex(any(), options.capture()); + + assertThat(options.getValue().getCollation()) + .isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build()); + } + + private DefaultIndexOperations indexOpsFor(Class type) { + return new DefaultIndexOperations(template, template.getCollectionName(type), type); + } + + @Data + static class Jedi { + @Field("firstname") String name; + } + + @org.springframework.data.mongodb.core.mapping.Document(collation = "de_AT") + static class Sith { + @Field("firstname") String name; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java new file mode 100644 index 000000000..5477de09e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2019 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 + * + * https://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.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import lombok.Data; + +import org.bson.Document; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.index.Index; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.query.Collation; + +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultReactiveIndexOperationsUnitTests { + + ReactiveMongoTemplate template; + + @Mock ReactiveMongoDatabaseFactory factory; + @Mock MongoDatabase db; + @Mock MongoCollection collection; + @Mock Publisher publisher; + + MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); + MappingMongoConverter converter; + MongoMappingContext mappingContext; + + @Before + public void setUp() { + + when(factory.getMongoDatabase()).thenReturn(db); + when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator); + when(db.getCollection(any(), any(Class.class))).thenReturn(collection); + when(collection.createIndex(any(), any(IndexOptions.class))).thenReturn(publisher); + + this.mappingContext = new MongoMappingContext(); + this.converter = spy(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)); + this.template = new ReactiveMongoTemplate(factory, converter); + } + + @Test // DATAMONGO-1854 + public void ensureIndexDoesNotSetCollectionIfNoDefaultDefined() { + + indexOpsFor(Jedi.class).ensureIndex(new Index("firstname", Direction.DESC)).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); + verify(collection).createIndex(any(), options.capture()); + + assertThat(options.getValue().getCollation()).isNull(); + } + + @Test // DATAMONGO-1854 + public void ensureIndexUsesDefaultCollationIfNoneDefinedInOptions() { + + indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC)).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); + verify(collection).createIndex(any(), options.capture()); + + assertThat(options.getValue().getCollation()) + .isEqualTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build()); + } + + @Test // DATAMONGO-1854 + public void ensureIndexDoesNotUseDefaultCollationIfExplicitlySpecifiedInTheIndex() { + + indexOpsFor(Sith.class).ensureIndex(new Index("firstname", Direction.DESC).collation(Collation.of("en_US"))) + .subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(IndexOptions.class); + verify(collection).createIndex(any(), options.capture()); + + assertThat(options.getValue().getCollation()) + .isEqualTo(com.mongodb.client.model.Collation.builder().locale("en_US").build()); + } + + private DefaultReactiveIndexOperations indexOpsFor(Class type) { + return new DefaultReactiveIndexOperations(template, template.getCollectionName(type), + new QueryMapper(template.getConverter()), type); + } + + @Data + static class Jedi { + @Field("firstname") String name; + } + + @org.springframework.data.mongodb.core.mapping.Document(collation = "de_AT") + static class Sith { + @Field("firstname") String name; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 819999da8..f99db1bab 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -88,14 +88,17 @@ import com.mongodb.MongoNamespace; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; import com.mongodb.client.AggregateIterable; +import com.mongodb.client.DistinctIterable; 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.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.DeleteOptions; import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndReplaceOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.MapReduceAction; import com.mongodb.client.model.ReplaceOptions; @@ -124,6 +127,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Mock FindIterable findIterable; @Mock AggregateIterable aggregateIterable; @Mock MapReduceIterable mapReduceIterable; + @Mock DistinctIterable distinctIterable; @Mock UpdateResult updateResult; @Mock DeleteResult deleteResult; @@ -143,12 +147,13 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { when(db.runCommand(any(), any(Class.class))).thenReturn(commandResultDocument); when(collection.find(any(org.bson.Document.class), any(Class.class))).thenReturn(findIterable); when(collection.mapReduce(any(), any(), eq(Document.class))).thenReturn(mapReduceIterable); - when(collection.count(any(Bson.class), any(CountOptions.class))).thenReturn(1L); // TODO: MongoDB 4 - fix me decprecated + when(collection.count(any(Bson.class), any(CountOptions.class))).thenReturn(1L); // TODO: MongoDB 4 - fix me when(collection.getNamespace()).thenReturn(new MongoNamespace("db.mock-collection")); when(collection.aggregate(any(List.class), any())).thenReturn(aggregateIterable); when(collection.withReadPreference(any())).thenReturn(collection); when(collection.replaceOne(any(), any(), any(ReplaceOptions.class))).thenReturn(updateResult); when(collection.withWriteConcern(any())).thenReturn(collectionWithWriteConcern); + when(collection.distinct(anyString(), any(Document.class), any())).thenReturn(distinctIterable); when(collectionWithWriteConcern.deleteOne(any(Bson.class), any())).thenReturn(deleteResult); when(findIterable.projection(any())).thenReturn(findIterable); when(findIterable.sort(any(org.bson.Document.class))).thenReturn(findIterable); @@ -166,6 +171,9 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { when(aggregateIterable.batchSize(anyInt())).thenReturn(aggregateIterable); when(aggregateIterable.map(any())).thenReturn(aggregateIterable); when(aggregateIterable.into(any())).thenReturn(Collections.emptyList()); + when(distinctIterable.collation(any())).thenReturn(distinctIterable); + when(distinctIterable.map(any())).thenReturn(distinctIterable); + when(distinctIterable.into(any())).thenReturn(Collections.emptyList()); this.mappingContext = new MongoMappingContext(); mappingContext.afterPropertiesSet(); @@ -1006,14 +1014,6 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { .containing("near.coordinates.[0]", 1D).containing("near.coordinates.[1]", 2D)); } - static class WithNamedFields { - - @Id String id; - - String name; - @Field("custom-named-field") String customName; - } - @Test // DATAMONGO-2155 public void saveVersionedEntityShouldCallUpdateCorrectly() { @@ -1085,6 +1085,308 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { .contains(new org.bson.Document("element", new Document("$gte", 100))); } + @Test // DATAMONGO-1854 + public void streamQueryShouldUseDefaultCollationWhenPresent() { + + template.stream(new BasicQuery("{}"), Sith.class).next(); + + verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findShouldNotUseCollationWhenNoDefaultPresent() { + + template.find(new BasicQuery("{'foo' : 'bar'}"), Jedi.class); + + verify(findIterable, never()).collation(any()); + } + + @Test // DATAMONGO-1854 + public void findShouldUseDefaultCollationWhenPresent() { + + template.find(new BasicQuery("{'foo' : 'bar'}"), Sith.class); + + verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findOneShouldUseDefaultCollationWhenPresent() { + + template.findOne(new BasicQuery("{'foo' : 'bar'}"), Sith.class); + + verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void existsShouldUseDefaultCollationWhenPresent() { + + template.exists(new BasicQuery("{}"), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(CountOptions.class); + verify(collection).count(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(equalTo(com.mongodb.client.model.Collation.builder().locale("de_AT").build()))); + } + + @Test // DATAMONGO-1854 + public void findAndModfiyShoudUseDefaultCollationWhenPresent() { + + template.findAndModify(new BasicQuery("{}"), new Update(), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndUpdateOptions.class); + verify(collection).findOneAndUpdate(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findAndRemoveShouldUseDefaultCollationWhenPresent() { + + template.findAndRemove(new BasicQuery("{}"), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndDeleteOptions.class); + verify(collection).findOneAndDelete(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void createCollectionShouldNotCollationIfNotPresent() { + + template.createCollection(AutogenerateableId.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class); + verify(db).createCollection(any(), options.capture()); + + Assertions.assertThat(options.getValue().getCollation()).isNull(); + } + + @Test // DATAMONGO-1854 + public void createCollectionShouldApplyDefaultCollation() { + + template.createCollection(Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class); + verify(db).createCollection(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void createCollectionShouldFavorExplicitOptionsOverDefaultCollation() { + + template.createCollection(Sith.class, CollectionOptions.just(Collation.of("en_US"))); + + ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class); + verify(db).createCollection(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("en_US").build())); + } + + @Test // DATAMONGO-1854 + public void createCollectionShouldUseDefaultCollationIfCollectionOptionsAreNull() { + + template.createCollection(Sith.class, null); + + ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class); + verify(db).createCollection(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void aggreateShouldUseDefaultCollationIfPresent() { + + template.aggregate(newAggregation(Sith.class, project("id")), AutogenerateableId.class, Document.class); + + verify(aggregateIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void aggreateShouldUseCollationFromOptionsEvenIfDefaultCollationIsPresent() { + + template.aggregateStream(newAggregation(Sith.class, project("id")).withOptions( + newAggregationOptions().collation(Collation.of("fr")).build()), AutogenerateableId.class, Document.class); + + verify(aggregateIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void aggreateStreamShouldUseDefaultCollationIfPresent() { + + template.aggregate(newAggregation(Sith.class, project("id")), AutogenerateableId.class, Document.class); + + verify(aggregateIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void aggreateStreamShouldUseCollationFromOptionsEvenIfDefaultCollationIsPresent() { + + template.aggregateStream(newAggregation(Sith.class, project("id")).withOptions( + newAggregationOptions().collation(Collation.of("fr")).build()), AutogenerateableId.class, Document.class); + + verify(aggregateIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void findAndReplaceShouldUseCollationWhenPresent() { + + template.findAndReplace(new BasicQuery("{}").collation(Collation.of("fr")), new AutogenerateableId()); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndReplaceOptions.class); + verify(collection).findOneAndReplace(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation().getLocale(), is("fr")); + } + + @Test // DATAMONGO-1854 + public void findOneWithSortShouldUseCollationWhenPresent() { + + template.findOne(new BasicQuery("{}").collation(Collation.of("fr")).with(Sort.by("id")), Sith.class); + + verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void findOneWithSortShouldUseDefaultCollationWhenPresent() { + + template.findOne(new BasicQuery("{}").with(Sort.by("id")), Sith.class); + + verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findAndReplaceShouldUseDefaultCollationWhenPresent() { + + template.findAndReplace(new BasicQuery("{}"), new Sith()); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndReplaceOptions.class); + verify(collection).findOneAndReplace(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation().getLocale(), is("de_AT")); + } + + @Test // DATAMONGO-18545 + public void findAndReplaceShouldUseCollationEvenIfDefaultCollationIsPresent() { + + template.findAndReplace(new BasicQuery("{}").collation(Collation.of("fr")), new Sith()); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndReplaceOptions.class); + verify(collection).findOneAndReplace(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation().getLocale(), is("fr")); + } + + @Test // DATAMONGO-1854 + public void findDistinctShouldUseDefaultCollationWhenPresent() { + + template.findDistinct(new BasicQuery("{}"), "name", Sith.class, String.class); + + verify(distinctIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findDistinctPreferCollationFromQueryOverDefaultCollation() { + + template.findDistinct(new BasicQuery("{}").collation(Collation.of("fr")), "name", Sith.class, String.class); + + verify(distinctIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void updateFirstShouldUseDefaultCollationWhenPresent() { + + template.updateFirst(new BasicQuery("{}"), Update.update("foo", "bar"), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + verify(collection).updateOne(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void updateFirstShouldPreferExplicitCollationOverDefaultCollation() { + + template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), Update.update("foo", "bar"), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + verify(collection).updateOne(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void updateMultiShouldUseDefaultCollationWhenPresent() { + + template.updateMulti(new BasicQuery("{}"), Update.update("foo", "bar"), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + verify(collection).updateMany(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void updateMultiShouldPreferExplicitCollationOverDefaultCollation() { + + template.updateMulti(new BasicQuery("{}").collation(Collation.of("fr")), Update.update("foo", "bar"), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + verify(collection).updateMany(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void removeShouldUseDefaultCollationWhenPresent() { + + template.remove(new BasicQuery("{}"), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(DeleteOptions.class); + verify(collection).deleteMany(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void removeShouldPreferExplicitCollationOverDefaultCollation() { + + template.remove(new BasicQuery("{}").collation(Collation.of("fr")), Sith.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(DeleteOptions.class); + verify(collection).deleteMany(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void mapReduceShouldUseDefaultCollationWhenPresent() { + + template.mapReduce("", "", "", MapReduceOptions.options(), Sith.class); + + verify(mapReduceIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void mapReduceShouldPreferExplicitCollationOverDefaultCollation() { + + template.mapReduce("", "", "", MapReduceOptions.options().collation(Collation.of("fr")), Sith.class); + + verify(mapReduceIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + class AutogenerateableId { @Id BigInteger id; @@ -1157,6 +1459,20 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { List grades; } + static class WithNamedFields { + + @Id String id; + + String name; + @Field("custom-named-field") String customName; + } + + @org.springframework.data.mongodb.core.mapping.Document(collation = "de_AT") + static class Sith { + + @Field("firstname") String name; + } + /** * Mocks out the {@link MongoTemplate#getDb()} method to return the {@link DB} mock instead of executing the actual * behaviour. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java index 5356d11a0..3161aa5d4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java @@ -514,7 +514,7 @@ public class ReactiveMongoTemplateTests { p = template.findAndModify(query, update, new FindAndModifyOptions().returnNew(true), Person.class).block(); assertThat(p.getAge()).isEqualTo(26); - p = template.findAndModify(query, update, null, Person.class, "person").block(); + p = template.findAndModify(query, update, FindAndModifyOptions.none(), Person.class, "person").block(); assertThat(p.getAge()).isEqualTo(26); p = template.findOne(query, Person.class).block(); assertThat(p.getAge()).isEqualTo(27); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 0cc335690..0acb22832 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -19,6 +19,7 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.mockito.Mockito.any; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import lombok.Data; import reactor.core.publisher.Mono; @@ -39,6 +40,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.reactivestreams.Publisher; + import org.springframework.beans.factory.annotation.Value; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.MongoTemplateUnitTests.AutogenerateableId; @@ -56,12 +58,15 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.client.model.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.DeleteOptions; import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndReplaceOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.UpdateOptions; import com.mongodb.reactivestreams.client.AggregatePublisher; +import com.mongodb.reactivestreams.client.DistinctPublisher; import com.mongodb.reactivestreams.client.FindPublisher; import com.mongodb.reactivestreams.client.MapReducePublisher; import com.mongodb.reactivestreams.client.MongoClient; @@ -88,6 +93,8 @@ public class ReactiveMongoTemplateUnitTests { @Mock Publisher runCommandPublisher; @Mock Publisher updatePublisher; @Mock Publisher findAndUpdatePublisher; + @Mock DistinctPublisher distinctPublisher; + @Mock Publisher deletePublisher; @Mock MapReducePublisher mapReducePublisher; MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); @@ -102,12 +109,20 @@ public class ReactiveMongoTemplateUnitTests { when(db.getCollection(any())).thenReturn(collection); when(db.getCollection(any(), any())).thenReturn(collection); when(db.runCommand(any(), any(Class.class))).thenReturn(runCommandPublisher); + when(db.createCollection(any(), any(CreateCollectionOptions.class))).thenReturn(runCommandPublisher); when(collection.find(any(Class.class))).thenReturn(findPublisher); when(collection.find(any(Document.class), any(Class.class))).thenReturn(findPublisher); when(collection.aggregate(anyList())).thenReturn(aggregatePublisher); when(collection.aggregate(anyList(), any(Class.class))).thenReturn(aggregatePublisher); when(collection.count(any(), any(CountOptions.class))).thenReturn(Mono.just(0L)); when(collection.updateOne(any(), any(), any(UpdateOptions.class))).thenReturn(updatePublisher); + when(collection.updateMany(any(Bson.class), any(), any())).thenReturn(updatePublisher); + when(collection.findOneAndUpdate(any(), any(), any(FindOneAndUpdateOptions.class))) + .thenReturn(findAndUpdatePublisher); + when(collection.findOneAndReplace(any(Bson.class), any(), any())).thenReturn(findPublisher); + when(collection.findOneAndDelete(any(), any(FindOneAndDeleteOptions.class))).thenReturn(findPublisher); + when(collection.distinct(anyString(), any(Document.class), any())).thenReturn(distinctPublisher); + when(collection.deleteMany(any(Bson.class), any())).thenReturn(deletePublisher); when(collection.findOneAndUpdate(any(), any(), any(FindOneAndUpdateOptions.class))) .thenReturn(findAndUpdatePublisher); when(collection.mapReduce(anyString(), anyString(), any())).thenReturn(mapReducePublisher); @@ -405,6 +420,255 @@ public class ReactiveMongoTemplateUnitTests { .contains(new org.bson.Document("element", new Document("$gte", 100))); } + @Test // DATAMONGO-1854 + public void findShouldNotUseCollationWhenNoDefaultPresent() { + + template.find(new BasicQuery("{'foo' : 'bar'}"), Jedi.class).subscribe(); + + verify(findPublisher, never()).collation(any()); + } + + @Test // DATAMONGO-1854 + public void findShouldUseDefaultCollationWhenPresent() { + + template.find(new BasicQuery("{'foo' : 'bar'}"), Sith.class).subscribe(); + + verify(findPublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findOneShouldUseDefaultCollationWhenPresent() { + + template.findOne(new BasicQuery("{'foo' : 'bar'}"), Sith.class).subscribe(); + + verify(findPublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void existsShouldUseDefaultCollationWhenPresent() { + + template.exists(new BasicQuery("{}"), Sith.class).subscribe(); + + verify(findPublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findAndModfiyShoudUseDefaultCollationWhenPresent() { + + template.findAndModify(new BasicQuery("{}"), new Update(), Sith.class).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndUpdateOptions.class); + verify(collection).findOneAndUpdate(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findAndRemoveShouldUseDefaultCollationWhenPresent() { + + template.findAndRemove(new BasicQuery("{}"), Sith.class).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndDeleteOptions.class); + verify(collection).findOneAndDelete(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void createCollectionShouldNotCollationIfNotPresent() { + + template.createCollection(AutogenerateableId.class).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class); + verify(db).createCollection(any(), options.capture()); + + Assertions.assertThat(options.getValue().getCollation()).isNull(); + } + + @Test // DATAMONGO-1854 + public void createCollectionShouldApplyDefaultCollation() { + + template.createCollection(Sith.class).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class); + verify(db).createCollection(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void createCollectionShouldFavorExplicitOptionsOverDefaultCollation() { + + template.createCollection(Sith.class, CollectionOptions.just(Collation.of("en_US"))).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class); + verify(db).createCollection(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("en_US").build())); + } + + @Test // DATAMONGO-1854 + public void createCollectionShouldUseDefaultCollationIfCollectionOptionsAreNull() { + + template.createCollection(Sith.class, null).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class); + verify(db).createCollection(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void aggreateShouldUseDefaultCollationIfPresent() { + + template.aggregate(newAggregation(Sith.class, project("id")), AutogenerateableId.class, Document.class).subscribe(); + + verify(aggregatePublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void aggreateShouldUseCollationFromOptionsEvenIfDefaultCollationIsPresent() { + + template + .aggregate( + newAggregation(Sith.class, project("id")) + .withOptions(newAggregationOptions().collation(Collation.of("fr")).build()), + AutogenerateableId.class, Document.class) + .subscribe(); + + verify(aggregatePublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-18545 + public void findAndReplaceShouldUseCollationWhenPresent() { + + template.findAndReplace(new BasicQuery("{}").collation(Collation.of("fr")), new Jedi()).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndReplaceOptions.class); + verify(collection).findOneAndReplace(any(Bson.class), any(), options.capture()); + + assertThat(options.getValue().getCollation().getLocale(), is("fr")); + } + + @Test // DATAMONGO-18545 + public void findAndReplaceShouldUseDefaultCollationWhenPresent() { + + template.findAndReplace(new BasicQuery("{}"), new Sith()).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndReplaceOptions.class); + verify(collection).findOneAndReplace(any(Bson.class), any(), options.capture()); + + assertThat(options.getValue().getCollation().getLocale(), is("de_AT")); + } + + @Test // DATAMONGO-18545 + public void findAndReplaceShouldUseCollationEvenIfDefaultCollationIsPresent() { + + template.findAndReplace(new BasicQuery("{}").collation(Collation.of("fr")), new MongoTemplateUnitTests.Sith()) + .subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(FindOneAndReplaceOptions.class); + verify(collection).findOneAndReplace(any(Bson.class), any(), options.capture()); + + assertThat(options.getValue().getCollation().getLocale(), is("fr")); + } + + @Test // DATAMONGO-1854 + public void findDistinctShouldUseDefaultCollationWhenPresent() { + + template.findDistinct(new BasicQuery("{}"), "name", Sith.class, String.class).subscribe(); + + verify(distinctPublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void findDistinctPreferCollationFromQueryOverDefaultCollation() { + + template.findDistinct(new BasicQuery("{}").collation(Collation.of("fr")), "name", Sith.class, String.class) + .subscribe(); + + verify(distinctPublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void updateFirstShouldUseDefaultCollationWhenPresent() { + + template.updateFirst(new BasicQuery("{}"), Update.update("foo", "bar"), Sith.class).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + verify(collection).updateOne(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void updateFirstShouldPreferExplicitCollationOverDefaultCollation() { + + template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), Update.update("foo", "bar"), Sith.class) + .subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + verify(collection).updateOne(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void updateMultiShouldUseDefaultCollationWhenPresent() { + + template.updateMulti(new BasicQuery("{}"), Update.update("foo", "bar"), Sith.class).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + verify(collection).updateMany(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void updateMultiShouldPreferExplicitCollationOverDefaultCollation() { + + template.updateMulti(new BasicQuery("{}").collation(Collation.of("fr")), Update.update("foo", "bar"), Sith.class) + .subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + verify(collection).updateMany(any(), any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + + @Test // DATAMONGO-1854 + public void removeShouldUseDefaultCollationWhenPresent() { + + template.remove(new BasicQuery("{}"), Sith.class).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(DeleteOptions.class); + verify(collection).deleteMany(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); + } + + @Test // DATAMONGO-1854 + public void removeShouldPreferExplicitCollationOverDefaultCollation() { + + template.remove(new BasicQuery("{}").collation(Collation.of("fr")), Sith.class).subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(DeleteOptions.class); + verify(collection).deleteMany(any(), options.capture()); + + assertThat(options.getValue().getCollation(), + is(com.mongodb.client.model.Collation.builder().locale("fr").build())); + } + @Data @org.springframework.data.mongodb.core.mapping.Document(collection = "star-wars") static class Person { @@ -434,6 +698,12 @@ public class ReactiveMongoTemplateUnitTests { @Field("firstname") String name; } + @org.springframework.data.mongodb.core.mapping.Document(collation = "de_AT") + static class Sith { + + @Field("firstname") String name; + } + static class EntityWithListOfSimple { List grades; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java index a663d8afc..1071d9fc7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java @@ -23,7 +23,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Arrays; -import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; import org.junit.Test; @@ -237,6 +237,24 @@ public class BasicMongoPersistentEntityUnitTests { assertThat(entity.getCollection()).isEqualTo("collectionName"); } + @Test // DATAMONGO-1854 + public void readsSimpleCollation() { + + BasicMongoPersistentEntity entity = new BasicMongoPersistentEntity<>( + ClassTypeInformation.from(WithSimpleCollation.class)); + + assertThat(entity.getCollation()).isEqualTo(org.springframework.data.mongodb.core.query.Collation.of("en_US")); + } + + @Test // DATAMONGO-1854 + public void readsDocumentCollation() { + + BasicMongoPersistentEntity entity = new BasicMongoPersistentEntity<>( + ClassTypeInformation.from(WithDocumentCollation.class)); + + assertThat(entity.getCollation()).isEqualTo(org.springframework.data.mongodb.core.query.Collation.of("en_US")); + } + @Document("contacts") class Contact {} @@ -283,10 +301,18 @@ public class BasicMongoPersistentEntityUnitTests { } // DATAMONGO-1874 - @Document("#{myProperty}") class MappedWithExtension {} + @Document(collation = "#{myCollation}") + class WithCollationFromSpEL {} + + @Document(collation = "en_US") + class WithSimpleCollation {} + + @Document(collation = "{ 'locale' : 'en_US' }") + class WithDocumentCollation {} + static class SampleExtension implements EvaluationContextExtension { /* @@ -304,7 +330,11 @@ public class BasicMongoPersistentEntityUnitTests { */ @Override public Map getProperties() { - return Collections.singletonMap("myProperty", "collectionName"); + + Map properties = new LinkedHashMap<>(); + properties.put("myProperty", "collectionName"); + properties.put("myCollation", "en_US"); + return properties; } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java index 3a9835007..8e1eafb7a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java @@ -16,7 +16,9 @@ package org.springframework.data.mongodb.repository.query; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -33,7 +35,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java index 82141b7a2..a117b77e9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java @@ -5,7 +5,7 @@ * 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 + * https://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, @@ -30,7 +30,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.data.mongodb.core.Person; import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ReactiveFindOperation.ReactiveFind; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 008943266..8eee93ee0 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -42,6 +42,7 @@ import org.springframework.data.mongodb.MongoTransactionManager; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.repository.Address; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.Person.Sex; @@ -485,6 +486,11 @@ public class SimpleMongoRepositoryTests { public String getIdAttribute() { return "id"; } + + @Override + public Collation getCollation() { + return null; + } } @Document diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java new file mode 100644 index 000000000..1a5feb0ca --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2019 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 + * + * https://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.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class SimpleMongoRepositoryUnitTests { + + SimpleMongoRepository repository; + @Mock MongoOperations mongoOperations; + @Mock MongoEntityInformation entityInformation; + + @Before + public void setUp() { + repository = new SimpleMongoRepository<>(entityInformation, mongoOperations); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToCountForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.count(Example.of(new TestDummy())); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).count(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToExistsForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.exists(Example.of(new TestDummy())); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).exists(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToFindForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.findAll(Example.of(new TestDummy())); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).find(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToFindWithSortForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.findAll(Example.of(new TestDummy()), Sort.by("nothing")); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).find(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToFindWithPageableForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.findAll(Example.of(new TestDummy()), PageRequest.of(1, 1, Sort.by("nothing"))); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).find(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToFindOneForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.findOne(Example.of(new TestDummy())); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).findOne(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + static class TestDummy { + + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java new file mode 100644 index 000000000..64e34255e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019. 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 + * + * https://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. + */ + +/* + * Copyright 2019 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 + * + * https://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.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class SimpleReactiveMongoRepositoryUnitTests { + + SimpleReactiveMongoRepository repository; + @Mock Mono mono; + @Mock Flux flux; + @Mock ReactiveMongoOperations mongoOperations; + @Mock MongoEntityInformation entityInformation; + + @Before + public void setUp() { + + when(mongoOperations.count(any(), any(), any())).thenReturn(mono); + when(mongoOperations.exists(any(), any(), any())).thenReturn(mono); + when(mongoOperations.find(any(), any(), any())).thenReturn(flux); + + repository = new SimpleReactiveMongoRepository<>(entityInformation, mongoOperations); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToCountForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.count(Example.of(new TestDummy())).subscribe(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).count(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToExistsForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.exists(Example.of(new TestDummy())).subscribe(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).exists(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToFindForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.findAll(Example.of(new TestDummy())).subscribe(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).find(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToFindWithSortForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.findAll(Example.of(new TestDummy()), Sort.by("nothing")).subscribe(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).find(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void shouldAddDefaultCollationToFindOneForExampleIfPresent() { + + Collation collation = Collation.of("en_US"); + + when(entityInformation.getCollation()).thenReturn(collation); + repository.findOne(Example.of(new TestDummy())).subscribe(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).find(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + static class TestDummy { + + } + +} diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index f22ef6a6c..c168d27f6 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1687,7 +1687,15 @@ Collation collation = Collation.of("fr") <1> <6> Specify whether to check whether text requires normalization and whether to perform normalization. ==== -Collations can be used to create collections and indexes. If you create a collection that specifies a collation, the collation is applied to index creation and queries unless you specify a different collation. A collation is valid for a whole operation and cannot be specified on a per-field basis, as the following example shows: +Collations can be used to create collections and indexes. If you create a collection that specifies a collation, the +collation is applied to index creation and queries unless you specify a different collation. A collation is valid for a +whole operation and cannot be specified on a per-field basis. + +Like other metadata, collations can be be derived from the domain type via the `collation` attribute of the `@Document` +annotation and will be applied directly when executing queries, creating collections or indexes. + +NOTE: Annotated collations will not be used when a collection is auto created by MongoDB on first interaction. This would +require additional store interaction delaying the entire process. Please use `MongoOperations.createCollection` for those cases. [source,java] ---- @@ -1738,7 +1746,7 @@ WARNING: Indexes are only used if the collation used for the operation matches t include::./mongo-json-schema.adoc[leveloffset=+1] -<> support `Collations` via the `@org.springframework.data.mongodb.repository.Query` annotation. +<> support `Collations` via the `collation` attribute of the `@Query` annotation. .Collation support for Repositories ==== @@ -1774,6 +1782,8 @@ and `Document` (eg. new Document("locale", "en_US")) NOTE: In case you enabled the automatic index creation for repository finder methods a potential static collation definition, as shown in (1) and (2), will be included when creating the index. + +TIP: The most specifc `Collation` outroules potentially defined others. Which means Method argument over query method annotation over doamin type annotation. ==== [[mongo.jsonSchema]]