From ac1873a16329afb961569fdb990c4df332c1396b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 15 Mar 2019 15:05:57 +0100 Subject: [PATCH] DATAMONGO-1854 - Allow Collation to be configured on entity level. The collation can now also be configured on entity level and gets applied via MongoTemplate. However one can alway override a default collation by adding the collation explicitly to either the Query or one of the Options available for various operations. When it comes to the repository level the hierarchy is method parameter over query annotation over entity metadata. Remove collation annotation in favor of attributes of Query / Document. Original pull request: #644. --- .../mongodb/core/DefaultIndexOperations.java | 38 +- .../core/DefaultReactiveIndexOperations.java | 51 ++- .../mongodb/core/FindAndModifyOptions.java | 35 ++ .../mongodb/core/FindAndReplaceOptions.java | 25 ++ .../data/mongodb/core/MongoTemplate.java | 146 ++++++-- .../mongodb/core/ReactiveMongoTemplate.java | 134 +++++-- .../core/ReactiveUpdateOperationSupport.java | 4 +- .../mapping/BasicMongoPersistentEntity.java | 52 ++- .../data/mongodb/core/mapping/Document.java | 10 +- .../core/mapping/MongoPersistentEntity.java | 26 +- .../data/mongodb/core/query/Collation.java | 20 ++ .../data/mongodb/repository/Query.java | 1 + .../query/MongoEntityInformation.java | 20 ++ .../MappingMongoEntityInformation.java | 10 + .../support/SimpleMongoRepository.java | 30 +- .../SimpleReactiveMongoRepository.java | 23 +- .../core/DefaultIndexOperationsUnitTests.java | 121 +++++++ ...faultReactiveIndexOperationsUnitTests.java | 126 +++++++ .../mongodb/core/MongoTemplateUnitTests.java | 334 +++++++++++++++++- .../core/ReactiveMongoTemplateTests.java | 2 +- .../core/ReactiveMongoTemplateUnitTests.java | 270 ++++++++++++++ .../BasicMongoPersistentEntityUnitTests.java | 36 +- .../query/AbstractMongoQueryUnitTests.java | 5 +- .../AbstractReactiveMongoQueryUnitTests.java | 3 +- .../support/SimpleMongoRepositoryTests.java | 6 + .../SimpleMongoRepositoryUnitTests.java | 139 ++++++++ ...impleReactiveMongoRepositoryUnitTests.java | 150 ++++++++ src/main/asciidoc/reference/mongodb.adoc | 14 +- 28 files changed, 1681 insertions(+), 150 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java 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]]