diff --git a/pom.xml b/pom.xml index d0c3937c5b..d0f8e5ce38 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-1854-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index bb7d9f03cc..755ec92b03 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-1854-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index b32dcba387..33cd4f6cc8 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-1854-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b611cf01a8..17adde9b45 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-1854-SNAPSHOT ../pom.xml 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 bfcd871e6d..4feef22e21 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 String ensureIndex(final IndexDefinition indexDefinition) { 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 List doInCollection(MongoCollection collection) private List getIndexData(MongoCursor cursor) { - List indexInfoList = new ArrayList(); + List indexInfoList = new ArrayList<>(); while (cursor.hasNext()) { @@ -217,4 +212,25 @@ public T execute(CollectionCallback callback) { 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 0e43f94ea9..dbfc2a3b5b 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 Mono ensureIndex(final IndexDefinition indexDefinition) { 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 @@ private MongoPersistentEntity lookupPersistentEntity(String collection) { .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 Flux getIndexInfo() { 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/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index 539fad2f3c..cc2cfe88f6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -21,12 +21,15 @@ import java.util.Collection; import java.util.Map; +import java.util.Optional; import org.bson.Document; + import org.springframework.core.convert.ConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -34,6 +37,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.lang.Nullable; @@ -176,6 +180,20 @@ private static Document parse(String source) { } } + public TypedOperations forType(@Nullable Class entityClass) { + + if (entityClass != null) { + + MongoPersistentEntity entity = context.getPersistentEntity(entityClass); + + if (entity != null) { + return new TypedEntityOperations(entity); + } + + } + return UntypedOperations.instance(); + } + /** * A representation of information about an entity. * @@ -263,7 +281,7 @@ default boolean isVersionedEntity() { /** * Returns whether the entity is considered to be new. - * + * * @return * @since 2.1.2 */ @@ -414,7 +432,7 @@ public T getBean() { return map; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.EntityOperations.Entity#isNew() */ @@ -585,7 +603,7 @@ public T getBean() { return propertyAccessor.getBean(); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.EntityOperations.Entity#isNew() */ @@ -698,4 +716,102 @@ public T incrementVersion() { return propertyAccessor.getBean(); } } + + /** + * Type-specific operations abstraction. + * + * @author Mark Paluch + * @param + * @since 2.2 + */ + interface TypedOperations { + + /** + * Return the optional {@link Collation} for the underlying entity. + * + * @return + */ + Optional getCollation(); + + /** + * Return the optional {@link Collation} from the given {@link Query} and fall back to the collation configured for + * the underlying entity. + * + * @return + */ + Optional getCollation(Query query); + } + + /** + * {@link TypedOperations} for generic entities that are not represented with {@link PersistentEntity} (e.g. custom + * conversions). + */ + @RequiredArgsConstructor + enum UntypedOperations implements TypedOperations { + + INSTANCE; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static TypedOperations instance() { + return (TypedOperations) INSTANCE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.TypedOperations#getCollation() + */ + @Override + public Optional getCollation() { + return Optional.empty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.TypedOperations#getCollation(org.springframework.data.mongodb.core.query.Query) + */ + @Override + public Optional getCollation(Query query) { + + if (query == null) { + return Optional.empty(); + } + + return query.getCollation(); + } + } + + /** + * {@link TypedOperations} backed by {@link MongoPersistentEntity}. + * + * @param + */ + @RequiredArgsConstructor + static class TypedEntityOperations implements TypedOperations { + + private final @NonNull MongoPersistentEntity entity; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.TypedOperations#getCollation() + */ + @Override + public Optional getCollation() { + return Optional.ofNullable(entity.getCollation()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.TypedOperations#getCollation(org.springframework.data.mongodb.core.query.Query) + */ + @Override + public Optional getCollation(Query query) { + + if (query.getCollation().isPresent()) { + return query.getCollation(); + } + + return Optional.ofNullable(entity.getCollation()); + } + } + } 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 62452a51e9..c9adf1f841 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(@Nullable Collation collation) { + throw new UnsupportedOperationException(ERROR_MSG); + } + }; + /** * Static factory method to create a FindAndModifyOptions instance * @@ -42,9 +67,19 @@ public static FindAndModifyOptions options() { 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}. - * + * * @param source can be {@literal null}. * @return new instance of {@link FindAndModifyOptions}. * @since 2.0 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 97c840af8d..1da434326e 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 static FindAndReplaceOptions options() { 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 53e2687773..3beda20b45 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 @@ -23,7 +23,18 @@ import lombok.RequiredArgsConstructor; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -33,6 +44,7 @@ import org.bson.conversions.Bson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -136,7 +148,17 @@ import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -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.ValidationAction; +import com.mongodb.client.model.ValidationLevel; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; @@ -404,6 +426,7 @@ public CloseableIterator stream(final Query query, final Class entityT return doStream(query, entityType, collectionName, entityType); } + @SuppressWarnings("ConstantConditions") protected CloseableIterator doStream(final Query query, final Class entityType, final String collectionName, Class returnType) { @@ -412,23 +435,18 @@ protected CloseableIterator doStream(final Query query, final Class en Assert.hasText(collectionName, "Collection name must not be null or empty!"); Assert.notNull(returnType, "ReturnType must not be null!"); - return execute(collectionName, new CollectionCallback>() { - - @Override - public CloseableIterator doInCollection(MongoCollection collection) - throws MongoException, DataAccessException { + return execute(collectionName, (CollectionCallback>) collection -> { - MongoPersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entityType); + MongoPersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entityType); - Document mappedFields = getMappedFieldsObject(query.getFieldsObject(), persistentEntity, returnType); - Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), persistentEntity); + Document mappedFields = getMappedFieldsObject(query.getFieldsObject(), persistentEntity, returnType); + Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), persistentEntity); - FindIterable cursor = new QueryCursorPreparer(query, entityType) - .prepare(collection.find(mappedQuery, Document.class).projection(mappedFields)); + FindIterable cursor = new QueryCursorPreparer(query, entityType) + .prepare(collection.find(mappedQuery, Document.class).projection(mappedFields)); - return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, - new ProjectingReadCallback<>(mongoConverter, entityType, returnType, collectionName)); - } + return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, + new ProjectingReadCallback<>(mongoConverter, entityType, returnType, collectionName)); }); } @@ -442,15 +460,12 @@ public String getCollectionName(Class entityClass) { * @see org.springframework.data.mongodb.core.MongoOperations#executeCommand(java.lang.String) */ @Override + @SuppressWarnings("ConstantConditions") public Document executeCommand(final String jsonCommand) { Assert.hasText(jsonCommand, "JsonCommand must not be null nor empty!"); - return execute(new DbCallback() { - public Document doInDB(MongoDatabase db) throws MongoException, DataAccessException { - return db.runCommand(Document.parse(jsonCommand), Document.class); - } - }); + return execute(db -> db.runCommand(Document.parse(jsonCommand), Document.class)); } /* @@ -458,6 +473,7 @@ public Document doInDB(MongoDatabase db) throws MongoException, DataAccessExcept * @see org.springframework.data.mongodb.core.MongoOperations#executeCommand(org.bson.Document) */ @Override + @SuppressWarnings("ConstantConditions") public Document executeCommand(final Document command) { Assert.notNull(command, "Command must not be null!"); @@ -469,7 +485,9 @@ public Document executeCommand(final Document command) { * (non-Javadoc) * @see org.springframework.data.mongodb.core.MongoOperations#executeCommand(org.bson.Document, com.mongodb.ReadPreference) */ + @Override + @SuppressWarnings("ConstantConditions") public Document executeCommand(Document command, @Nullable ReadPreference readPreference) { Assert.notNull(command, "Command must not be null!"); @@ -515,7 +533,7 @@ protected void executeQuery(Query query, String collectionName, DocumentCallback sortObject, fieldsObject, collectionName); } - this.executeQueryInternal(new FindCallback(queryObject, fieldsObject), preparer, documentCallbackHandler, + this.executeQueryInternal(new FindCallback(queryObject, fieldsObject, null), preparer, documentCallbackHandler, collectionName); } @@ -602,7 +620,7 @@ public void setSessionSynchronization(SessionSynchronization sessionSynchronizat * @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 +631,14 @@ public MongoCollection createCollection(Class entityClass, @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(() -> Optional.ofNullable(collectionOptions).flatMap(CollectionOptions::getCollation), + () -> operations.forType(entityClass).getCollation()) // + .map(options::collation).orElse(options); + + return doCreateCollection(operations.determineCollectionName(entityClass), convertToDocument(options, entityClass)); } /* @@ -643,15 +667,12 @@ public MongoCollection createCollection(final String collectionName, * (non-Javadoc) * @see org.springframework.data.mongodb.core.MongoOperations#getCollection(java.lang.String) */ + @SuppressWarnings("ConstantConditions") public MongoCollection getCollection(final String collectionName) { Assert.notNull(collectionName, "CollectionName must not be null!"); - return execute(new DbCallback>() { - public MongoCollection doInDB(MongoDatabase db) throws MongoException, DataAccessException { - return db.getCollection(collectionName, Document.class); - } - }); + return execute(db -> db.getCollection(collectionName, Document.class)); } /* @@ -666,20 +687,19 @@ public boolean collectionExists(Class entityClass) { * (non-Javadoc) * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#getCollection(java.lang.String) */ + @SuppressWarnings("ConstantConditions") public boolean collectionExists(final String collectionName) { Assert.notNull(collectionName, "CollectionName must not be null!"); - return execute(new DbCallback() { - public Boolean doInDB(MongoDatabase db) throws MongoException, DataAccessException { + return execute(db -> { - for (String name : db.listCollectionNames()) { - if (name.equals(collectionName)) { - return true; - } + for (String name : db.listCollectionNames()) { + if (name.equals(collectionName)) { + return true; } - return false; } + return false; }); } @@ -786,8 +806,11 @@ public T findOne(Query query, Class entityClass, String collectionName) { 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(), + operations.forType(entityClass).getCollation(query).map(Collation::toMongoCollation).orElse(null), + entityClass); } else { query.limit(1); List results = find(query, entityClass, collectionName); @@ -806,6 +829,7 @@ public boolean exists(Query query, String collectionName) { } @Override + @SuppressWarnings("ConstantConditions") public boolean exists(Query query, @Nullable Class entityClass, String collectionName) { if (query == null) { @@ -815,8 +839,8 @@ public boolean exists(Query query, @Nullable Class entityClass, String collec Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), getPersistentEntity(entityClass)); - return execute(collectionName, - new ExistsCallback(mappedQuery, query.getCollation().map(Collation::toMongoCollation).orElse(null))); + return execute(collectionName, new ExistsCallback(mappedQuery, + operations.forType(entityClass).getCollation(query).map(Collation::toMongoCollation).orElse(null))); } // Find methods that take a Query to express the query and that return a List of objects. @@ -906,7 +930,11 @@ public List findDistinct(Query query, String field, String collectionName DistinctIterable iterable = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType); - return query.getCollation().map(Collation::toMongoCollation).map(iterable::collation).orElse(iterable); + return operations.forType(entityClass) // + .getCollation(query) // + .map(Collation::toMongoCollation) // + .map(iterable::collation) // + .orElse(iterable); }); if (resultClass == Object.class || mongoDriverCompatibleType != resultClass) { @@ -1063,7 +1091,9 @@ public T findAndModify(Query query, Update update, FindAndModifyOptions opti "Both Query and FindAndModifyOptions define a collation. Please provide the collation only via one of the two."); }); - query.getCollation().ifPresent(optionsToUse::collation); + if (!options.getCollation().isPresent()) { + operations.forType(entityClass).getCollation(query).ifPresent(optionsToUse::collation); + } return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); @@ -1096,8 +1126,8 @@ public T findAndReplace(Query query, S replacement, FindAndReplaceOptions 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); + operations.forType(entityType).getCollation(query).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 +1148,8 @@ public T findAndRemove(Query query, Class entityClass, String collectionN 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), operations.forType(entityClass).getCollation(query).orElse(null), + entityClass); } @Override @@ -1158,6 +1189,7 @@ public long count(Query query, @Nullable Class entityClass, String collection return doCount(collectionName, document, options); } + @SuppressWarnings("ConstantConditions") protected long doCount(String collectionName, Document filter, CountOptions options) { if (LOGGER.isDebugEnabled()) { @@ -1448,26 +1480,25 @@ protected T doSave(String collectionName, T objectToSave, MongoWriter wri return saved; } + @SuppressWarnings("ConstantConditions") protected Object insertDocument(final String collectionName, final Document document, final Class entityClass) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Inserting Document containing fields: {} in collection: {}", document.keySet(), collectionName); } - return execute(collectionName, new CollectionCallback() { - public Object doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, - entityClass, document, null); - WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - - if (writeConcernToUse == null) { - collection.insertOne(document); - } else { - collection.withWriteConcern(writeConcernToUse).insertOne(document); - } + return execute(collectionName, collection -> { + MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, entityClass, + document, null); + WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - return operations.forEntity(document).getId(); + if (writeConcernToUse == null) { + collection.insertOne(document); + } else { + collection.withWriteConcern(writeConcernToUse).insertOne(document); } + + return operations.forEntity(document).getId(); }); } @@ -1584,6 +1615,7 @@ public UpdateResult updateMulti(final Query query, final Update update, Class return doUpdate(collectionName, query, update, entityClass, false, true); } + @SuppressWarnings("ConstantConditions") protected UpdateResult doUpdate(final String collectionName, final Query query, final UpdateDefinition update, @Nullable final Class entityClass, final boolean upsert, final boolean multi) { @@ -1591,61 +1623,60 @@ protected UpdateResult doUpdate(final String collectionName, final Query query, Assert.notNull(query, "Query must not be null!"); Assert.notNull(update, "Update must not be null!"); - return execute(collectionName, new CollectionCallback() { - public UpdateResult doInCollection(MongoCollection collection) - throws MongoException, DataAccessException { + return execute(collectionName, collection -> { - MongoPersistentEntity entity = entityClass == null ? null : getPersistentEntity(entityClass); + MongoPersistentEntity entity = entityClass == null ? null : getPersistentEntity(entityClass); - increaseVersionForUpdateIfNecessary(entity, update); + increaseVersionForUpdateIfNecessary(entity, update); - UpdateOptions opts = new UpdateOptions(); - opts.upsert(upsert); + UpdateOptions opts = new UpdateOptions(); + opts.upsert(upsert); - if (update.hasArrayFilters()) { - opts.arrayFilters( - update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList())); - } + if (update.hasArrayFilters()) { + opts.arrayFilters(update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList())); + } - Document queryObj = new Document(); + Document queryObj = new Document(); - if (query != null) { + if (query != null) { + queryObj.putAll(queryMapper.getMappedObject(query.getQueryObject(), entity)); + } - queryObj.putAll(queryMapper.getMappedObject(query.getQueryObject(), entity)); - query.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation); - } + operations.forType(entityClass) // + .getCollation(query) // + .map(Collation::toMongoCollation) // + .ifPresent(opts::collation); - Document updateObj = update instanceof MappedUpdate ? update.getUpdateObject() - : updateMapper.getMappedObject(update.getUpdateObject(), entity); + Document updateObj = update instanceof MappedUpdate ? update.getUpdateObject() + : updateMapper.getMappedObject(update.getUpdateObject(), entity); - if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) { - queryObj.put("$isolated", 1); - } + if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) { + queryObj.put("$isolated", 1); + } - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Calling update using query: {} and update: {} in collection: {}", - serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName); - } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Calling update using query: {} and update: {} in collection: {}", serializeToJsonSafely(queryObj), + serializeToJsonSafely(updateObj), collectionName); + } - MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName, - entityClass, updateObj, queryObj); - WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName, entityClass, + updateObj, queryObj); + WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - collection = writeConcernToUse != null ? collection.withWriteConcern(writeConcernToUse) : collection; + collection = writeConcernToUse != null ? collection.withWriteConcern(writeConcernToUse) : collection; - if (!UpdateMapper.isUpdateObject(updateObj)) { + if (!UpdateMapper.isUpdateObject(updateObj)) { - ReplaceOptions replaceOptions = new ReplaceOptions(); - replaceOptions.collation(opts.getCollation()); - replaceOptions.upsert(opts.isUpsert()); + ReplaceOptions replaceOptions = new ReplaceOptions(); + replaceOptions.collation(opts.getCollation()); + replaceOptions.upsert(opts.isUpsert()); - return collection.replaceOne(queryObj, updateObj, replaceOptions); + return collection.replaceOne(queryObj, updateObj, replaceOptions); + } else { + if (multi) { + return collection.updateMany(queryObj, updateObj, opts); } else { - if (multi) { - return collection.updateMany(queryObj, updateObj, opts); - } else { - return collection.updateOne(queryObj, updateObj, opts); - } + return collection.updateOne(queryObj, updateObj, opts); } } }); @@ -1698,6 +1729,7 @@ public DeleteResult remove(Query query, Class entityClass, String collectionN return doRemove(collectionName, query, entityClass, true); } + @SuppressWarnings("ConstantConditions") protected DeleteResult doRemove(final String collectionName, final Query query, @Nullable final Class entityClass, boolean multi) { @@ -1707,53 +1739,53 @@ protected DeleteResult doRemove(final String collectionName, final Query que final MongoPersistentEntity entity = getPersistentEntity(entityClass); final Document queryObject = queryMapper.getMappedObject(query.getQueryObject(), entity); - return execute(collectionName, new CollectionCallback() { - - public DeleteResult doInCollection(MongoCollection collection) - throws MongoException, DataAccessException { + return execute(collectionName, collection -> { - maybeEmitEvent(new BeforeDeleteEvent<>(queryObject, entityClass, collectionName)); + maybeEmitEvent(new BeforeDeleteEvent<>(queryObject, entityClass, collectionName)); - Document removeQuery = queryObject; + Document removeQuery = queryObject; - DeleteOptions options = new DeleteOptions(); - query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation); + DeleteOptions options = new DeleteOptions(); - MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName, - entityClass, null, queryObject); + operations.forType(entityClass) // + .getCollation(query) // + .map(Collation::toMongoCollation) // + .ifPresent(options::collation); - WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName, entityClass, + null, queryObject); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Remove using query: {} in collection: {}.", - new Object[] { serializeToJsonSafely(removeQuery), collectionName }); - } + WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - if (query.getLimit() > 0 || query.getSkip() > 0) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Remove using query: {} in collection: {}.", + new Object[] { serializeToJsonSafely(removeQuery), collectionName }); + } - MongoCursor cursor = new QueryCursorPreparer(query, entityClass) - .prepare(collection.find(removeQuery).projection(MappedDocument.getIdOnlyProjection())) // - .iterator(); + if (query.getLimit() > 0 || query.getSkip() > 0) { - Set ids = new LinkedHashSet<>(); - while (cursor.hasNext()) { - ids.add(MappedDocument.of(cursor.next()).getId()); - } + MongoCursor cursor = new QueryCursorPreparer(query, entityClass) + .prepare(collection.find(removeQuery).projection(MappedDocument.getIdOnlyProjection())) // + .iterator(); - removeQuery = MappedDocument.getIdIn(ids); + Set ids = new LinkedHashSet<>(); + while (cursor.hasNext()) { + ids.add(MappedDocument.of(cursor.next()).getId()); } - MongoCollection collectionToUse = writeConcernToUse != null - ? collection.withWriteConcern(writeConcernToUse) - : collection; + removeQuery = MappedDocument.getIdIn(ids); + } - DeleteResult result = multi ? collectionToUse.deleteMany(removeQuery, options) - : collectionToUse.deleteOne(removeQuery, options); + MongoCollection collectionToUse = writeConcernToUse != null + ? collection.withWriteConcern(writeConcernToUse) + : collection; - maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName)); + DeleteResult result = multi ? collectionToUse.deleteMany(removeQuery, options) + : collectionToUse.deleteOne(removeQuery, options); - return result; - } + maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName)); + + return result; }); } @@ -1764,8 +1796,10 @@ public List findAll(Class entityClass) { @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(), + operations.forType(entityClass).getCollation().map(Collation::toMongoCollation).orElse(null)), + null, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); } @Override @@ -1879,6 +1913,10 @@ public List mapReduce(Query query, Class domainType, String inputColle } } + if (!collation.isPresent()) { + collation = operations.forType(domainType).getCollation(); + } + mapReduce = collation.map(Collation::toMongoCollation).map(mapReduce::collation).orElse(mapReduce); List mappedResults = new ArrayList<>(); @@ -2092,6 +2130,7 @@ protected AggregationResults aggregate(Aggregation aggregation, String co return doAggregate(aggregation, collectionName, outputType, contextToUse); } + @SuppressWarnings("ConstantConditions") protected AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, AggregationOperationContext context) { @@ -2123,8 +2162,15 @@ protected AggregationResults doAggregate(Aggregation aggregation, String List rawResult = new ArrayList<>(); + Class domainType = aggregation instanceof TypedAggregation ? ((TypedAggregation) aggregation).getInputType() + : null; + + Optional collation = Optionals.firstNonEmpty(options::getCollation, + () -> operations.forType(domainType) // + .getCollation()); + 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) { @@ -2142,6 +2188,7 @@ protected AggregationResults doAggregate(Aggregation aggregation, String }); } + @SuppressWarnings("ConstantConditions") protected CloseableIterator aggregateStream(Aggregation aggregation, String collectionName, Class outputType, @Nullable AggregationOperationContext context) { @@ -2165,16 +2212,19 @@ protected CloseableIterator aggregateStream(Aggregation aggregation, Stri 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()); - } + Class domainType = aggregation instanceof TypedAggregation ? ((TypedAggregation) aggregation).getInputType() + : null; + + Optionals.firstNonEmpty(options::getCollation, // + () -> operations.forType(domainType).getCollation()) // + .map(Collation::toMongoCollation) // + .ifPresent(cursor::collation); return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, readCallback); }); @@ -2267,15 +2317,14 @@ protected String replaceWithResourceIfNecessary(String function) { * (non-Javadoc) * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#getCollectionNames() */ + @SuppressWarnings("ConstantConditions") public Set getCollectionNames() { - return execute(new DbCallback>() { - public Set doInDB(MongoDatabase db) throws MongoException, DataAccessException { - Set result = new LinkedHashSet<>(); - for (String name : db.listCollectionNames()) { - result.add(name); - } - return result; + return execute(db -> { + Set result = new LinkedHashSet<>(); + for (String name : db.listCollectionNames()) { + result.add(name); } + return result; }); } @@ -2307,53 +2356,51 @@ protected , T> E maybeEmitEvent(E event) { * @param collectionOptions * @return the collection that was created */ - protected MongoCollection doCreateCollection(final String collectionName, - final Document collectionOptions) { - return execute(new DbCallback>() { - public MongoCollection doInDB(MongoDatabase db) throws MongoException, DataAccessException { + @SuppressWarnings("ConstantConditions") + protected MongoCollection doCreateCollection(String collectionName, Document collectionOptions) { + return execute(db -> { - CreateCollectionOptions co = new CreateCollectionOptions(); + CreateCollectionOptions co = new CreateCollectionOptions(); - if (collectionOptions.containsKey("capped")) { - co.capped((Boolean) collectionOptions.get("capped")); - } - if (collectionOptions.containsKey("size")) { - co.sizeInBytes(((Number) collectionOptions.get("size")).longValue()); - } - if (collectionOptions.containsKey("max")) { - co.maxDocuments(((Number) collectionOptions.get("max")).longValue()); - } - - if (collectionOptions.containsKey("collation")) { - co.collation(IndexConverters.fromDocument(collectionOptions.get("collation", Document.class))); - } + if (collectionOptions.containsKey("capped")) { + co.capped((Boolean) collectionOptions.get("capped")); + } + if (collectionOptions.containsKey("size")) { + co.sizeInBytes(((Number) collectionOptions.get("size")).longValue()); + } + if (collectionOptions.containsKey("max")) { + co.maxDocuments(((Number) collectionOptions.get("max")).longValue()); + } - if (collectionOptions.containsKey("validator")) { + if (collectionOptions.containsKey("collation")) { + co.collation(IndexConverters.fromDocument(collectionOptions.get("collation", Document.class))); + } - com.mongodb.client.model.ValidationOptions options = new com.mongodb.client.model.ValidationOptions(); + if (collectionOptions.containsKey("validator")) { - if (collectionOptions.containsKey("validationLevel")) { - options.validationLevel(ValidationLevel.fromString(collectionOptions.getString("validationLevel"))); - } - if (collectionOptions.containsKey("validationAction")) { - options.validationAction(ValidationAction.fromString(collectionOptions.getString("validationAction"))); - } + com.mongodb.client.model.ValidationOptions options = new com.mongodb.client.model.ValidationOptions(); - options.validator(collectionOptions.get("validator", Document.class)); - co.validationOptions(options); + if (collectionOptions.containsKey("validationLevel")) { + options.validationLevel(ValidationLevel.fromString(collectionOptions.getString("validationLevel"))); + } + if (collectionOptions.containsKey("validationAction")) { + options.validationAction(ValidationAction.fromString(collectionOptions.getString("validationAction"))); } - db.createCollection(collectionName, co); + options.validator(collectionOptions.get("validator", Document.class)); + co.validationOptions(options); + } - MongoCollection coll = db.getCollection(collectionName, Document.class); + db.createCollection(collectionName, co); - // TODO: Emit a collection created event - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Created collection [{}]", - coll.getNamespace() != null ? coll.getNamespace().getCollectionName() : collectionName); - } - return coll; + MongoCollection coll = db.getCollection(collectionName, Document.class); + + // TODO: Emit a collection created event + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Created collection [{}]", + coll.getNamespace() != null ? coll.getNamespace().getCollectionName() : collectionName); } + return coll; }); } @@ -2368,6 +2415,23 @@ public MongoCollection doInDB(MongoDatabase db) throws MongoException, * @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. + * @since 2.2 + */ + @SuppressWarnings("ConstantConditions") + 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 +2442,7 @@ protected T doFindOne(String collectionName, Document query, Document fields 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 +2493,7 @@ protected List doFind(String collectionName, Document query, Document 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 +2516,7 @@ List doFind(String collectionName, Document query, Document fields, Cl 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); } @@ -2531,6 +2595,7 @@ Document getMappedValidator(Validator validator, Class domainType) { * @param entityClass the parameterized type of the returned list. * @return the List of converted objects. */ + @SuppressWarnings("ConstantConditions") protected T doFindAndRemove(String collectionName, Document query, Document fields, Document sort, @Nullable Collation collation, Class entityClass) { @@ -2548,6 +2613,7 @@ protected T doFindAndRemove(String collectionName, Document query, Document new ReadDocumentCallback<>(readerToUse, entityClass, collectionName), collectionName); } + @SuppressWarnings("ConstantConditions") protected T doFindAndModify(String collectionName, Document query, Document fields, Document sort, Class entityClass, Update update, @Nullable FindAndModifyOptions options) { @@ -2819,15 +2885,21 @@ private static class FindOneCallback implements CollectionCallback { 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 +2928,27 @@ private static class FindCallback implements CollectionCallback 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; } } @@ -2982,7 +3061,7 @@ private static class FindAndReplaceCallback implements CollectionCallback type; - public QueryCursorPreparer(@Nullable Query query, @Nullable Class type) { + public QueryCursorPreparer(Query query, @Nullable Class type) { this.query = query; this.type = type; @@ -3156,20 +3235,18 @@ public QueryCursorPreparer(@Nullable Query query, @Nullable Class type) { */ public FindIterable prepare(FindIterable cursor) { - if (query == null) { - return cursor; - } + FindIterable cursorToUse = cursor; + + operations.forType(type).getCollation(query) // + .map(Collation::toMongoCollation) // + .ifPresent(cursorToUse::collation); 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()); 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 56fc38336e..328e92e308 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 @@ -25,7 +25,17 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuples; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; @@ -70,7 +80,16 @@ 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 +132,29 @@ 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 +646,7 @@ public Mono createMono(String collectionName, ReactiveCollectionCallback< * @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 +655,17 @@ public Mono> createCollection(Class entityClass */ 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(() -> Optional.ofNullable(collectionOptions).flatMap(CollectionOptions::getCollation), + () -> operations.forType(entityClass).getCollation()) // + .map(options::collation).orElse(options); + return doCreateCollection(determineCollectionName(entityClass), - convertToCreateCollectionOptions(collectionOptions, entityClass)); + convertToCreateCollectionOptions(options, entityClass)); } /* @@ -719,7 +765,7 @@ public Mono findOne(Query query, Class entityClass, String collectionN if (ObjectUtils.isEmpty(query.getSortObject())) { return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass, - query.getCollation().orElse(null)); + operations.forType(entityClass).getCollation(query).orElse(null)); } query.limit(1); @@ -762,8 +808,8 @@ public Mono exists(Query query, @Nullable Class entityClass, String LOGGER.debug("exists: {} in collection: {}", serializeToJsonSafely(filter), collectionName); } - findPublisher = query.getCollation().map(Collation::toMongoCollation).map(findPublisher::collation) - .orElse(findPublisher); + findPublisher = operations.forType(entityClass).getCollation(query).map(Collation::toMongoCollation) + .map(findPublisher::collation).orElse(findPublisher); return findPublisher.limit(1); }).hasElements(); @@ -849,8 +895,10 @@ public Flux findDistinct(Query query, String field, String collectionName } DistinctPublisher publisher = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType); - - return query.getCollation().map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher); + return operations.forType(entityClass).getCollation(query) // + .map(Collation::toMongoCollation) // + .map(publisher::collation) // + .orElse(publisher); }); if (resultClass == Object.class || mongoDriverCompatibleType != resultClass) { @@ -960,11 +1008,12 @@ protected Flux aggregate(Aggregation aggregation, String collectionName, } 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 +1022,9 @@ private Flux aggregateAndMap(MongoCollection collection, List operations.forType(inputType).getCollation()) // + .map(Collation::toMongoCollation) // + .ifPresent(cursor::collation); return Flux.from(cursor).map(readCallback::doWith); } @@ -1072,6 +1121,8 @@ public Mono findAndModify(Query query, Update update, FindAndModifyOption 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 +1130,10 @@ public Mono findAndModify(Query query, Update update, FindAndModifyOption "Both Query and FindAndModifyOptions define a collation. Please provide the collation only via one of the two."); }); - query.getCollation().ifPresent(optionsToUse::collation); + if (!optionsToUse.getCollation().isPresent()) { + operations.forType(entityClass).getCollation(query).ifPresent(optionsToUse::collation); + ; + } return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); @@ -1112,8 +1166,8 @@ public Mono findAndReplace(Query query, S replacement, FindAndReplaceO 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); + operations.forType(entityType).getCollation(query).map(Collation::toMongoCollation).orElse(null), entityType, + mappedReplacement, options, resultType); } /* @@ -1130,8 +1184,10 @@ public Mono findAndRemove(Query query, Class entityClass) { */ public Mono findAndRemove(Query query, Class entityClass, String collectionName) { + operations.forType(entityClass).getCollation(query); return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(), - getMappedSortObject(query, entityClass), query.getCollation().orElse(null), entityClass); + getMappedSortObject(query, entityClass), operations.forType(entityClass).getCollation(query).orElse(null), + entityClass); } /* @@ -1164,22 +1220,22 @@ public Mono count(Query query, @Nullable Class entityClass, String coll return createMono(collectionName, collection -> { - Document filter = query == null ? null - : queryMapper.getMappedObject(query.getQueryObject(), - entityClass == null ? null : mappingContext.getPersistentEntity(entityClass)); + Document filter = queryMapper.getMappedObject(query.getQueryObject(), + entityClass == null ? null : mappingContext.getPersistentEntity(entityClass)); CountOptions options = new CountOptions(); - if (query != null) { - query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation); + query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation); - if (query.getLimit() > 0) { - options.limit(query.getLimit()); - } - if (query.getSkip() > 0) { - options.skip((int) query.getSkip()); - } + if (query.getLimit() > 0) { + options.limit(query.getLimit()); + } + if (query.getSkip() > 0) { + options.skip((int) query.getSkip()); } + operations.forType(entityClass).getCollation(query).map(Collation::toMongoCollation) // + .ifPresent(options::collation); + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Executing count: {} in collection: {}", serializeToJsonSafely(filter), collectionName); } @@ -1647,7 +1703,9 @@ protected Mono doUpdate(String collectionName, Query query, @Nulla MongoCollection collectionToUse = prepareCollection(collection, writeConcernToUse); UpdateOptions updateOptions = new UpdateOptions().upsert(upsert); - query.getCollation().map(Collation::toMongoCollation).ifPresent(updateOptions::collation); + operations.forType(entityClass).getCollation(query) // + .map(Collation::toMongoCollation) // + .ifPresent(updateOptions::collation); if (update.hasArrayFilters()) { updateOptions.arrayFilters(update.getArrayFilters().stream().map(ArrayFilter::asDocument) @@ -1811,7 +1869,10 @@ protected Mono doRemove(String collectionName, Query query, @N null, removeQuery); DeleteOptions deleteOptions = new DeleteOptions(); - query.getCollation().map(Collation::toMongoCollation).ifPresent(deleteOptions::collation); + + operations.forType(entityClass).getCollation(query) // + .map(Collation::toMongoCollation) // + .ifPresent(deleteOptions::collation); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); MongoCollection collectionToUse = prepareCollection(collection, writeConcernToUse); @@ -2381,7 +2442,9 @@ protected Mono doFindAndModify(String collectionName, Document query, Doc 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 +2844,13 @@ public Publisher doInCollection(MongoCollection collection) 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(); @@ -3011,10 +3075,10 @@ public GeoResult doWith(Document object) { */ class QueryFindPublisherPreparer implements FindPublisherPreparer { - private final @Nullable Query query; + private final Query query; private final @Nullable Class type; - QueryFindPublisherPreparer(@Nullable Query query, @Nullable Class type) { + QueryFindPublisherPreparer(Query query, @Nullable Class type) { this.query = query; this.type = type; @@ -3023,13 +3087,10 @@ class QueryFindPublisherPreparer implements FindPublisherPreparer { @SuppressWarnings("deprecation") public FindPublisher prepare(FindPublisher findPublisher) { - if (query == null) { - return findPublisher; - } - - FindPublisher findPublisherToUse; - - findPublisherToUse = query.getCollation().map(Collation::toMongoCollation).map(findPublisher::collation) + FindPublisher findPublisherToUse = operations.forType(type) // + .getCollation(query) // + .map(Collation::toMongoCollation) // + .map(findPublisher::collation) // .orElse(findPublisher); Meta meta = query.getMeta(); @@ -3205,4 +3266,5 @@ public void onApplicationEvent(MappingContextEvent event) { } } } + } 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 c4b507e383..96f09e0793 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 @@ public Mono findAndModify() { 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 @@ public Mono findAndModify() { @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 757909d26f..3f41090396 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 typeInformation) { this.collection = StringUtils.hasText(document.collection()) ? document.collection() : fallback; this.language = StringUtils.hasText(document.language()) ? document.language() : ""; - this.expression = detectExpression(document); + this.expression = detectExpression(document.collection()); + this.collation = document.collation(); + this.collationExpression = detectExpression(document.collation()); } else { this.collection = fallback; this.language = ""; this.expression = null; + this.collation = null; + this.collationExpression = null; } } @@ -126,6 +133,33 @@ public boolean hasTextScoreProperty() { return getTextScoreProperty() != null; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.MongoPersistentEntity#getCollation() + */ + @Override + public org.springframework.data.mongodb.core.query.Collation getCollation() { + + Object collationValue = collationExpression != null ? expression.getValue(getEvaluationContext(null), String.class) + : this.collation; + + if (collationValue == null) { + return null; + } + + if (collationValue instanceof org.bson.Document) { + return org.springframework.data.mongodb.core.query.Collation.from((org.bson.Document) collationValue); + } + + if (collationValue instanceof org.springframework.data.mongodb.core.query.Collation) { + return org.springframework.data.mongodb.core.query.Collation.class.cast(collationValue); + } + + return StringUtils.hasText(collationValue.toString()) + ? org.springframework.data.mongodb.core.query.Collation.parse(collationValue.toString()) + : null; + } + /* * (non-Javadoc) * @see org.springframework.data.mapping.model.BasicPersistentEntity#verify() @@ -246,24 +280,20 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul } /** - * Returns a SpEL {@link Expression} frór the collection String expressed in the given {@link Document} annotation if - * present or {@literal null} otherwise. Will also return {@literal null} it the collection {@link String} evaluates - * to a {@link LiteralExpression} (indicating that no subsequent evaluation is necessary). + * Returns a SpEL {@link Expression} if the given {@link String} is actually an expression that does not evaluate to a + * {@link LiteralExpression} (indicating that no subsequent evaluation is necessary). * - * @param document can be {@literal null} + * @param potentialExpression can be {@literal null} * @return */ @Nullable - private static Expression detectExpression(Document document) { + private static Expression detectExpression(@Nullable String potentialExpression) { - String collection = document.collection(); - - if (!StringUtils.hasText(collection)) { + if (!StringUtils.hasText(potentialExpression)) { return null; } - Expression expression = PARSER.parseExpression(document.collection(), ParserContext.TEMPLATE_EXPRESSION); - + Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION); return expression instanceof LiteralExpression ? null : expression; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java index 1f634e2e78..eb9a6d5190 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java @@ -60,9 +60,17 @@ /** * Defines the default language to be used with this document. * - * @since 1.6 * @return + * @since 1.6 */ String language() default ""; + /** + * Defines the collation to apply when executing a query or creating indexes. + * + * @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/core/mapping/MongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java index 40a7d135e3..35fcf1ef4b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java @@ -36,17 +36,17 @@ public interface MongoPersistentEntity extends PersistentEntity extends PersistentEntity{ 'locale' : '?0' } . + * @return never {@literal null}. + * @throws IllegalArgumentException if {@literal collation} is null. + * @since 2.2 + */ + public static Collation parse(String collation) { + + Assert.notNull(collation, "Collation must not be null!"); + + return StringUtils.trimLeadingWhitespace(collation).startsWith("{") ? from(Document.parse(collation)) + : of(collation); + } + /** * Create new {@link Collation} from values in {@link Document}. * @@ -386,6 +403,26 @@ public String toString() { return toDocument().toJson(); } + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Collation that = (Collation) o; + return this.toDocument().equals(that.toDocument()); + } + + @Override + public int hashCode() { + return toDocument().hashCode(); + } + private Collation copy() { Collation collation = new Collation(locale); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java index 8f902aead3..0bc3e1618c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java @@ -95,4 +95,34 @@ * @since 2.1 */ String sort() default ""; + + /** + * Defines the collation to apply when executing the query. + * + *
+	 * // Fixed value
+	 * @Query(collation = "en_US")
+	 * List findAllByFixedCollation();
+	 *
+	 * // Fixed value as Document
+	 * @Query(collation = "{ 'locale' :  'en_US' }")
+	 * List findAllByFixedJsonCollation();
+	 *
+	 * // Dynamic value as String
+	 * @Query(collation = "?0")
+	 * List findAllByDynamicCollation(String collation);
+	 *
+	 * // Dynamic value as Document
+	 * @Query(collation = "{ 'locale' :  ?0 }")
+	 * List findAllByDynamicJsonCollation(String collation);
+	 *
+	 * // SpEL expression
+	 * @Query(collation = "?#{[0]}")
+	 * List 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/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index 4784b15f87..03dbc78ff9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.repository.query; import org.bson.Document; + import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; @@ -27,8 +28,10 @@ import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagingGeoNearExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** @@ -44,17 +47,24 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { private final MongoQueryMethod method; private final MongoOperations operations; private final ExecutableFind executableFind; + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; /** * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}. * * @param method must not be {@literal null}. * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. */ - public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations) { + public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, SpelExpressionParser expressionParser, + QueryMethodEvaluationContextProvider evaluationContextProvider) { Assert.notNull(operations, "MongoOperations must not be null!"); Assert.notNull(method, "MongoQueryMethod must not be null!"); + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); this.method = method; this.operations = operations; @@ -63,6 +73,8 @@ public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations) { Class type = metadata.getCollectionEntity().getType(); this.executableFind = operations.query(type); + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; } /* @@ -86,6 +98,7 @@ public Object execute(Object[] parameters) { applyQueryMetaAttributesWhenPresent(query); query = applyAnnotatedDefaultSortIfPresent(query); + query = applyAnnotatedCollationIfPresent(query, accessor); ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); Class typeToRead = processor.getReturnedType().getTypeToRead(); @@ -154,6 +167,21 @@ Query applyAnnotatedDefaultSortIfPresent(Query query) { return QueryUtils.decorateSort(query, Document.parse(method.getAnnotatedSort())); } + /** + * If present apply a {@link org.springframework.data.mongodb.core.query.Collation} derived from the + * {@link org.springframework.data.repository.query.QueryMethod} the given {@link Query}. + * + * @param query must not be {@literal null}. + * @param accessor the {@link ParameterAccessor} used to obtain parameter placeholder replacement values. + * @return + * @since 2.2 + */ + Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor accessor) { + + return QueryUtils.applyCollation(query, method.hasAnnotatedCollation() ? method.getAnnotatedCollation() : null, + accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider); + } + /** * Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to * {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java index d5c341569b..343965d703 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java @@ -20,6 +20,7 @@ import org.bson.Document; import org.reactivestreams.Publisher; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mongodb.core.MongoOperations; @@ -33,8 +34,10 @@ import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingConverter; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingExecution; import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** @@ -50,6 +53,8 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { private final ReactiveMongoOperations operations; private final EntityInstantiators instantiators; private final FindWithProjection findOperationWithProjection; + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; /** * Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and @@ -57,15 +62,22 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { * * @param method must not be {@literal null}. * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. */ - public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations) { + public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { Assert.notNull(method, "MongoQueryMethod must not be null!"); Assert.notNull(operations, "ReactiveMongoOperations must not be null!"); + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); this.method = method; this.operations = operations; this.instantiators = new EntityInstantiators(); + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; MongoEntityMetadata metadata = method.getEntityInformation(); Class type = metadata.getCollectionEntity().getType(); @@ -105,12 +117,15 @@ private Object executeDeferred(Object[] parameters) { private Object execute(MongoParameterAccessor parameterAccessor) { - Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), parameterAccessor)); + ConvertingParameterAccessor convertingParamterAccessor = new ConvertingParameterAccessor(operations.getConverter(), + parameterAccessor); + Query query = createQuery(convertingParamterAccessor); applyQueryMetaAttributesWhenPresent(query); query = applyAnnotatedDefaultSortIfPresent(query); + query = applyAnnotatedCollationIfPresent(query, convertingParamterAccessor); - ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor); + ResultProcessor processor = method.getResultProcessor().withDynamicProjection(convertingParamterAccessor); Class typeToRead = processor.getReturnedType().getTypeToRead(); FindWithQuery find = typeToRead == null // @@ -119,7 +134,7 @@ private Object execute(MongoParameterAccessor parameterAccessor) { String collection = method.getEntityInformation().getCollectionName(); - ReactiveMongoQueryExecution execution = getExecution(parameterAccessor, + ReactiveMongoQueryExecution execution = getExecution(convertingParamterAccessor, new ResultProcessingConverter(processor, operations, instantiators), find); return execution.execute(query, processor.getReturnedType().getDomainType(), collection); @@ -195,6 +210,21 @@ Query applyAnnotatedDefaultSortIfPresent(Query query) { return QueryUtils.decorateSort(query, Document.parse(method.getAnnotatedSort())); } + /** + * If present apply a {@link org.springframework.data.mongodb.core.query.Collation} derived from the + * {@link org.springframework.data.repository.query.QueryMethod} the given {@link Query}. + * + * @param query must not be {@literal null}. + * @param accessor the {@link ParameterAccessor} used to obtain parameter placeholder replacement values. + * @return + * @since 2.2 + */ + Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor accessor) { + + return QueryUtils.applyCollation(query, method.hasAnnotatedCollation() ? method.getAnnotatedCollation() : null, + accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider); + } + /** * Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to * {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index f44e799e14..6d98f30bce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -29,6 +29,7 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.convert.MongoWriter; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.util.TypeInformation; @@ -135,6 +136,15 @@ public TextCriteria getFullText() { return delegate.getFullText(); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation() + */ + @Override + public Collation getCollation() { + return delegate.getCollation(); + } + /** * Converts the given value with the underlying {@link MongoWriter}. * 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 51fa02479d..22242a4f90 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 @@ default boolean isVersioned() { 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/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index f41a1fa120..43bdfcc22c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -18,6 +18,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.lang.Nullable; @@ -57,6 +58,15 @@ public interface MongoParameterAccessor extends ParameterAccessor { @Nullable TextCriteria getFullText(); + /** + * Returns the {@link Collation} to be used for the query. + * + * @return {@literal null} if not set. + * @since 2.2 + */ + @Nullable + Collation getCollation(); + /** * Returns the raw parameter values of the underlying query method. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java index 5db8b24fb9..e7800498a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java @@ -23,6 +23,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.repository.Near; import org.springframework.data.mongodb.repository.query.MongoParameters.MongoParameter; @@ -45,12 +46,13 @@ public class MongoParameters extends Parameters private final int maxDistanceIndex; private final @Nullable Integer fullTextIndex; private final @Nullable Integer nearIndex; + private final @Nullable Integer collationIndex; /** * Creates a new {@link MongoParameters} instance from the given {@link Method} and {@link MongoQueryMethod}. * * @param method must not be {@literal null}. - * @param queryMethod must not be {@literal null}. + * @param isGeoNearMethod indicate if this is a geo spatial query method */ public MongoParameters(Method method, boolean isGeoNearMethod) { @@ -64,6 +66,7 @@ public MongoParameters(Method method, boolean isGeoNearMethod) { this.rangeIndex = getTypeIndex(parameterTypeInfo, Range.class, Distance.class); this.maxDistanceIndex = this.rangeIndex == -1 ? getTypeIndex(parameterTypeInfo, Distance.class, null) : -1; + this.collationIndex = getTypeIndex(parameterTypeInfo, Collation.class, null); int index = findNearIndexInParameters(method); if (index == -1 && isGeoNearMethod) { @@ -74,7 +77,7 @@ public MongoParameters(Method method, boolean isGeoNearMethod) { } private MongoParameters(List parameters, int maxDistanceIndex, @Nullable Integer nearIndex, - @Nullable Integer fullTextIndex, int rangeIndex) { + @Nullable Integer fullTextIndex, int rangeIndex, @Nullable Integer collationIndex) { super(parameters); @@ -82,6 +85,7 @@ private MongoParameters(List parameters, int maxDistanceIndex, @ this.fullTextIndex = fullTextIndex; this.maxDistanceIndex = maxDistanceIndex; this.rangeIndex = rangeIndex; + this.collationIndex = collationIndex; } private final int getNearIndex(List> parameterTypes) { @@ -111,11 +115,11 @@ private int findNearIndexInParameters(Method method) { MongoParameter param = createParameter(MethodParameter.forParameter(p)); if (param.isManuallyAnnotatedNearParameter()) { - if(index == -1) { + if (index == -1) { index = param.getIndex(); } else { - throw new IllegalStateException(String.format("Found multiple @Near annotations ond method %s! Only one allowed!", - method.toString())); + throw new IllegalStateException( + String.format("Found multiple @Near annotations ond method %s! Only one allowed!", method.toString())); } } @@ -132,8 +136,6 @@ protected MongoParameter createParameter(MethodParameter parameter) { return new MongoParameter(parameter); } - - public int getDistanceRangeIndex() { return -1; } @@ -158,7 +160,7 @@ public int getNearIndex() { } /** - * Returns ths inde of the parameter to be used as a textquery param + * Returns the index of the parameter to be used as a textquery param * * @return * @since 1.6 @@ -183,13 +185,24 @@ public int getRangeIndex() { return rangeIndex; } + /** + * Returns the index of the {@link Collation} parameter or -1 if not present. + * + * @return -1 if not set. + * @since 2.2 + */ + public int getCollationParameterIndex() { + return collationIndex != null ? collationIndex.intValue() : -1; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) */ @Override protected MongoParameters createFrom(List parameters) { - return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex); + return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex, + this.collationIndex); } private int getTypeIndex(List> parameterTypes, Class type, @Nullable Class componentType) { @@ -241,7 +254,7 @@ class MongoParameter extends Parameter { @Override public boolean isSpecialParameter() { return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter() - || TextCriteria.class.isAssignableFrom(getType()); + || TextCriteria.class.isAssignableFrom(getType()) || Collation.class.isAssignableFrom(getType()); } private boolean isNearParameter() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index 3cf2e58bd2..2364f68628 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -22,6 +22,7 @@ import org.springframework.data.domain.Range.Bound; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Term; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.ParametersParameterAccessor; @@ -67,7 +68,8 @@ public Range getDistanceRange() { } int maxDistanceIndex = mongoParameters.getMaxDistanceIndex(); - Bound maxDistance = maxDistanceIndex == -1 ? Bound.unbounded() : Bound.inclusive((Distance) getValue(maxDistanceIndex)); + Bound maxDistance = maxDistanceIndex == -1 ? Bound.unbounded() + : Bound.inclusive((Distance) getValue(maxDistanceIndex)); return Range.of(Bound.unbounded(), maxDistance); } @@ -134,6 +136,20 @@ protected TextCriteria potentiallyConvertFullText(Object fullText) { ClassUtils.getShortName(fullText.getClass()))); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation() + */ + @Override + public Collation getCollation() { + + if (method.getParameters().getCollationParameterIndex() == -1) { + return null; + } + + return getValue(method.getParameters().getCollationParameterIndex()); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index 9bc7d00a1c..79caf54ec5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -322,6 +322,30 @@ public String getAnnotatedSort() { "Expected to find @Query annotation but did not. Make sure to check hasAnnotatedSort() before.")); } + /** + * Check if the query method is decorated with an non empty {@link Query#collation()}. + * + * @return true if method annotated with {@link Query} having an non empty collation attribute. + * @since 2.2 + */ + public boolean hasAnnotatedCollation() { + return lookupQueryAnnotation().map(it -> !it.collation().isEmpty()).orElse(false); + } + + /** + * Get the collation value extracted from the {@link Query} annotation. + * + * @return the {@link Query#collation()} value. + * @throws IllegalStateException if method not annotated with {@link Query}. Make sure to check + * {@link #hasAnnotatedQuery()} first. + * @since 2.2 + */ + public String getAnnotatedCollation() { + + return lookupQueryAnnotation().map(Query::collation).orElseThrow(() -> new IllegalStateException( + "Expected to find @Query annotation but did not. Make sure to check hasAnnotatedCollation() before.")); + } + @SuppressWarnings("unchecked") private Optional doFindAnnotation(Class annotationType) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index 16d2aeb6d0..7a3f3e4b80 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -17,6 +17,7 @@ import org.bson.Document; import org.bson.json.JsonParseException; + import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoTemplate; @@ -26,10 +27,12 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.StringUtils; /** @@ -52,10 +55,13 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { * * @param method must not be {@literal null}. * @param mongoOperations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. */ - public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations) { + public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations); + super(method, mongoOperations, expressionParser, evaluationContextProvider); this.processor = method.getResultProcessor(); this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java index c9cca1266b..d73f0b4152 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java @@ -15,21 +15,40 @@ */ package org.springframework.data.mongodb.repository.query; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import org.aopalliance.intercept.MethodInterceptor; import org.bson.Document; + import org.springframework.aop.framework.ProxyFactory; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.NumberUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Query} support * offered by repositories. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.1 * @currentRead Assassin's Apprentice - Robin Hobb */ class QueryUtils { + private static final ParameterBindingDocumentCodec CODEC = new ParameterBindingDocumentCodec(); + + private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + /** * Decorate {@link Query} and add a default sort expression to the given {@link Query}. Attributes of the given * {@code sort} may be overwritten by the sort explicitly defined by the {@link Query} itself. @@ -58,4 +77,65 @@ static Query decorateSort(Query query, Document defaultSort) { return (Query) factory.getProxy(); } + + /** + * Apply a collation extracted from the given {@literal collationExpression} to the given {@link Query}. Potentially + * replace parameter placeholders with values from the {@link ConvertingParameterAccessor accessor}. + * + * @param query must not be {@literal null}. + * @param collationExpression must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @return the {@link Query} having proper {@link Collation}. + * @see Query#collation(Collation) + * @since 2.2 + */ + static Query applyCollation(Query query, @Nullable String collationExpression, ConvertingParameterAccessor accessor, + MongoParameters parameters, SpelExpressionParser expressionParser, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + + if (accessor.getCollation() != null) { + return query.collation(accessor.getCollation()); + } + + if (collationExpression == null) { + return query; + } + + if (StringUtils.trimLeadingWhitespace(collationExpression).startsWith("{")) { + + ParameterBindingContext bindingContext = new ParameterBindingContext((accessor::getBindableValue), + expressionParser, evaluationContextProvider.getEvaluationContext(parameters, accessor.getValues())); + + return query.collation(Collation.from(CODEC.decode(collationExpression, bindingContext))); + } + + Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(collationExpression); + if (!matcher.find()) { + return query.collation(Collation.parse(collationExpression)); + } + + String placeholder = matcher.group(); + Object placeholderValue = accessor.getBindableValue(computeParameterIndex(placeholder)); + + if (collationExpression.startsWith("?")) { + + if (placeholderValue instanceof String) { + return query.collation(Collation.parse(placeholderValue.toString())); + } + if (placeholderValue instanceof Locale) { + return query.collation(Collation.of((Locale) placeholderValue)); + } + if (placeholderValue instanceof Document) { + return query.collation(Collation.from((Document) placeholderValue)); + } + throw new IllegalArgumentException(String.format("Collation must be a String, Locale or Document but was %s", + ObjectUtils.nullSafeClassName(placeholderValue))); + } + + return query.collation(Collation.parse(collationExpression.replace(placeholder, placeholderValue.toString()))); + } + + private static int computeParameterIndex(String parameter) { + return NumberUtils.parseNumber(parameter.replace("?", ""), Integer.class); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java index 3fd1fd253c..f5af831a7d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java @@ -17,6 +17,7 @@ import org.bson.Document; import org.bson.json.JsonParseException; + import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -25,10 +26,12 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.StringUtils; /** @@ -50,10 +53,13 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery { * * @param method must not be {@literal null}. * @param mongoOperations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. */ - public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations) { + public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations); + super(method, mongoOperations, expressionParser, evaluationContextProvider); this.processor = method.getResultProcessor(); this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java index 1850576e0e..5e1e73040c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java @@ -18,6 +18,7 @@ import org.bson.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; @@ -78,7 +79,7 @@ public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod meth ReactiveMongoOperations mongoOperations, SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations); + super(method, mongoOperations, expressionParser, evaluationContextProvider); Assert.notNull(query, "Query must not be null!"); Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index 42a1c9fffc..7b045e8f98 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -18,6 +18,7 @@ import org.bson.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; @@ -77,7 +78,7 @@ public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOpera public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations, SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations); + super(method, mongoOperations, expressionParser, evaluationContextProvider); Assert.notNull(query, "Query must not be null!"); Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java index d470eecebd..62753898d6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java @@ -21,12 +21,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.index.Index; import org.springframework.data.mongodb.core.index.IndexOperationsProvider; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.repository.query.MongoEntityMetadata; import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery; import org.springframework.data.repository.core.support.QueryCreationListener; @@ -93,6 +95,14 @@ public void onCreation(PartTreeMongoQuery query) { } } + if (query.getQueryMethod().hasAnnotatedCollation()) { + + String collation = query.getQueryMethod().getAnnotatedCollation(); + if (!collation.contains("?")) { + index = index.collation(Collation.parse(collation)); + } + } + MongoEntityMetadata metadata = query.getQueryMethod().getEntityInformation(); indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index); LOG.debug(String.format("Created %s!", index)); 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 594cd94870..9f81af2e1f 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 @@ 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 Object getVersion(T entity) { 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/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 2920285d36..eeff22eb47 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -190,7 +190,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, } else if (queryMethod.hasAnnotatedQuery()) { return new StringBasedMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider); } else { - return new PartTreeMongoQuery(queryMethod, operations); + return new PartTreeMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 0e24b1ea7b..c5001dcf23 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -180,7 +180,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, } else if (queryMethod.hasAnnotatedQuery()) { return new ReactiveStringBasedMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider); } else { - return new ReactivePartTreeMongoQuery(queryMethod, operations); + return new ReactivePartTreeMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider); } } } 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 678a7a0c2b..2ad380c785 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 Page findAll(Pageable pageable) { 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 Page findAll(final Example example, Pageable pageable 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 List findAll(Example example, Sort sort) { 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 Optional findOne(Example example) { 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 long count(Example example) { 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 boolean exists(Example example) { 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 3bb7025f60..eec5c2c154 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 Mono findOne(Example example) { 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 Mono exists(Example example) { 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 Flux findAll(Example example, Sort sort) { 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 Mono count(Example example) { 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 0000000000..01f696dad1 --- /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 0000000000..5477de09eb --- /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 819999da84..f99db1bab6 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.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 void setUp() { 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 void setUp() { 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 void geoNearShouldMapGeoJsonPointCorrectly() { .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 void findAndModifyShouldApplyArrayFilters() { .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 @@ static class EntityWithListOfSimple { 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 5356d11a05..3161aa5d4d 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 void testFindAndUpdate() { 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 974fdb6bc8..29e519e085 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.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; @@ -40,6 +41,7 @@ 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.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.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; @@ -87,6 +92,8 @@ public class ReactiveMongoTemplateUnitTests { @Mock Publisher runCommandPublisher; @Mock Publisher updatePublisher; @Mock Publisher findAndUpdatePublisher; + @Mock DistinctPublisher distinctPublisher; + @Mock Publisher deletePublisher; MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); MappingMongoConverter converter; @@ -100,13 +107,20 @@ public void setUp() { 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.findOneAndUpdate(any(), any(), any(FindOneAndUpdateOptions.class))).thenReturn(findAndUpdatePublisher); + 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(findPublisher.projection(any())).thenReturn(findPublisher); when(findPublisher.limit(anyInt())).thenReturn(findPublisher); when(findPublisher.collation(any())).thenReturn(findPublisher); @@ -402,6 +416,255 @@ public void findAndModifyShouldApplyArrayFilters() { .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 { @@ -431,6 +694,12 @@ static class Jedi { @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 a663d8afcb..1071d9fc7b 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.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 void usesEvaluationContextExtensionInDynamicDocumentName() { 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 @@ static class DocumentWithComposedAnnotation {} } // 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 String getExtensionId() { */ @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 290103e4d1..8e1eafb7ac 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 @@ -15,8 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -24,6 +23,7 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.Locale; import java.util.Optional; import org.bson.Document; @@ -52,6 +52,7 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.MongoRepository; @@ -59,6 +60,8 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; import com.mongodb.client.result.DeleteResult; @@ -121,7 +124,7 @@ public void testDeleteExecutionReturnsZeroWhenWriteResultIsNull() { MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class); query.setDeleteQuery(true); - assertThat(query.execute(new Object[] { "fake" }), is(0L)); + assertThat(query.execute(new Object[] { "fake" })).isEqualTo(0L); } @Test // DATAMONGO-566, DATAMONGO-978 @@ -133,7 +136,7 @@ public void testDeleteExecutionReturnsNrDocumentsDeletedFromWriteResult() { MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class); query.setDeleteQuery(true); - assertThat(query.execute(new Object[] { "fake" }), is(100L)); + assertThat(query.execute(new Object[] { "fake" })).isEqualTo(100L); verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons")); } @@ -148,7 +151,8 @@ public void metadataShouldNotBeAddedToQueryWhenNotPresent() { verify(executableFind).as(Person.class); verify(withQueryMock).matching(captor.capture()); - assertThat(captor.getValue().getMeta().getComment(), nullValue()); + assertThat(captor.getValue().getMeta().getComment()).isNull(); + ; } @Test // DATAMONGO-957 @@ -162,7 +166,7 @@ public void metadataShouldBeAddedToQueryCorrectly() { verify(executableFind).as(Person.class); verify(withQueryMock).matching(captor.capture()); - assertThat(captor.getValue().getMeta().getComment(), is("comment")); + assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment"); } @Test // DATAMONGO-957 @@ -176,7 +180,7 @@ public void metadataShouldBeAddedToCountQueryCorrectly() { verify(executableFind).as(Person.class); verify(withQueryMock, atLeast(1)).matching(captor.capture()); - assertThat(captor.getValue().getMeta().getComment(), is("comment")); + assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment"); } @Test // DATAMONGO-957, DATAMONGO-1783 @@ -190,7 +194,7 @@ public void metadataShouldBeAddedToStringBasedQueryCorrectly() { verify(executableFind).as(Person.class); verify(withQueryMock).matching(captor.capture()); - assertThat(captor.getValue().getMeta().getComment(), is("comment")); + assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment"); } @Test // DATAMONGO-1057 @@ -208,8 +212,8 @@ public void slicedExecutionShouldRetainNrOfElementsToSkip() { verify(executableFind, times(2)).as(Person.class); verify(withQueryMock, times(2)).matching(captor.capture()); - assertThat(captor.getAllValues().get(0).getSkip(), is(0L)); - assertThat(captor.getAllValues().get(1).getSkip(), is(10L)); + assertThat(captor.getAllValues().get(0).getSkip()).isZero(); + assertThat(captor.getAllValues().get(1).getSkip()).isEqualTo(10); } @Test // DATAMONGO-1057 @@ -227,8 +231,8 @@ public void slicedExecutionShouldIncrementLimitByOne() { verify(executableFind, times(2)).as(Person.class); verify(withQueryMock, times(2)).matching(captor.capture()); - assertThat(captor.getAllValues().get(0).getLimit(), is(11)); - assertThat(captor.getAllValues().get(1).getLimit(), is(11)); + assertThat(captor.getAllValues().get(0).getLimit()).isEqualTo(11); + assertThat(captor.getAllValues().get(1).getLimit()).isEqualTo(11); } @Test // DATAMONGO-1057 @@ -247,8 +251,8 @@ public void slicedExecutionShouldRetainSort() { verify(withQueryMock, times(2)).matching(captor.capture()); Document expectedSortObject = new Document().append("bar", -1); - assertThat(captor.getAllValues().get(0).getSortObject(), is(expectedSortObject)); - assertThat(captor.getAllValues().get(1).getSortObject(), is(expectedSortObject)); + assertThat(captor.getAllValues().get(0).getSortObject()).isEqualTo(expectedSortObject); + assertThat(captor.getAllValues().get(1).getSortObject()).isEqualTo(expectedSortObject); } @Test // DATAMONGO-1080 @@ -260,7 +264,7 @@ public void doesNotTryToPostProcessQueryResultIntoWrapperType() { AbstractMongoQuery query = createQueryForMethod("findByLastname", String.class); - assertThat(query.execute(new Object[] { "lastname" }), is(reference)); + assertThat(query.execute(new Object[] { "lastname" })).isEqualTo(reference); } @Test // DATAMONGO-1865 @@ -272,7 +276,7 @@ public void limitingSingleEntityQueryCallsFirst() { AbstractMongoQuery query = createQueryForMethod("findFirstByLastname", String.class).setLimitingQuery(true); - assertThat(query.execute(new Object[] { "lastname" }), is(reference)); + assertThat(query.execute(new Object[] { "lastname" })).isEqualTo(reference); } @Test // DATAMONGO-1872 @@ -294,7 +298,7 @@ public void usesAnnotatedSortWhenPresent() { ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); verify(withQueryMock).matching(captor.capture()); - assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", 1)))); + assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("age", 1)); } @Test // DATAMONGO-1979 @@ -305,7 +309,127 @@ public void usesExplicitSortOverridesAnnotatedSortWhenPresent() { ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); verify(withQueryMock).matching(captor.capture()); - assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", -1)))); + assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("age", -1)); + } + + @Test // DATAMONGO-1854 + public void shouldApplyStaticAnnotatedCollation() { + + createQueryForMethod("findWithCollationUsingSpimpleStringValueByFirstName", String.class) // + .execute(new Object[] { "dalinar" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyStaticAnnotatedCollationAsDocument() { + + createQueryForMethod("findWithCollationUsingDocumentByFirstName", String.class) // + .execute(new Object[] { "dalinar" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationAsString() { + + createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) // + .execute(new Object[] { "dalinar", "en_US" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationAsDocument() { + + createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) // + .execute(new Object[] { "dalinar", new Document("locale", "en_US") }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationAsLocale() { + + createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) // + .execute(new Object[] { "dalinar", Locale.US }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1854 + public void shouldThrowExceptionOnNonParsableCollation() { + + createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) // + .execute(new Object[] { "dalinar", 100 }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationIn() { + + createQueryForMethod("findWithCollationUsingPlaceholderInDocumentByFirstName", String.class, String.class) // + .execute(new Object[] { "dalinar", "en_US" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyCollationParameter() { + + Collation collation = Collation.of("en_US"); + createQueryForMethod("findWithCollationParameterByFirstName", String.class, Collation.class) // + .execute(new Object[] { "dalinar", collation }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void collationParameterShouldOverrideAnnotation() { + + Collation collation = Collation.of("de_AT"); + createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) // + .execute(new Object[] { "dalinar", collation }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() { + + createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) // + .execute(new Object[] { "dalinar", null }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); } private MongoQueryFake createQueryForMethod(String methodName, Class... paramTypes) { @@ -333,7 +457,7 @@ private static class MongoQueryFake extends AbstractMongoQuery { private boolean isLimitingQuery; public MongoQueryFake(MongoQueryMethod method, MongoOperations operations) { - super(method, operations); + super(method, operations, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT); } @Override @@ -400,6 +524,23 @@ private interface Repo extends MongoRepository { @org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }") List findByAge(Integer age, Sort page); + + @org.springframework.data.mongodb.repository.Query(collation = "en_US") + List findWithCollationUsingSpimpleStringValueByFirstName(String firstname); + + @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }") + List findWithCollationUsingDocumentByFirstName(String firstname); + + @org.springframework.data.mongodb.repository.Query(collation = "?1") + List findWithCollationUsingPlaceholderByFirstName(String firstname, Object collation); + + @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1' }") + List findWithCollationUsingPlaceholderInDocumentByFirstName(String firstname, String collation); + + List findWithCollationParameterByFirstName(String firstname, Collation collation); + + @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }") + List findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation); } // DATAMONGO-1872 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 new file mode 100644 index 0000000000..a117b77e9e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java @@ -0,0 +1,304 @@ +/* + * 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.query; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Locale; + +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.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; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * @author Christoph Strobl + * @currentRead Way of Kings - Brandon Sanderson + */ +@RunWith(MockitoJUnitRunner.class) +public class AbstractReactiveMongoQueryUnitTests { + + @Mock ReactiveMongoOperations mongoOperationsMock; + @Mock BasicMongoPersistentEntity persitentEntityMock; + @Mock MongoMappingContext mappingContextMock; + + @Mock ReactiveFind executableFind; + @Mock FindWithQuery withQueryMock; + + @Before + public void setUp() { + + doReturn("persons").when(persitentEntityMock).getCollection(); + doReturn(persitentEntityMock).when(mappingContextMock).getPersistentEntity(Mockito.any(Class.class)); + doReturn(persitentEntityMock).when(mappingContextMock).getRequiredPersistentEntity(Mockito.any(Class.class)); + doReturn(Person.class).when(persitentEntityMock).getType(); + + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContextMock); + converter.afterPropertiesSet(); + + doReturn(converter).when(mongoOperationsMock).getConverter(); + + doReturn(executableFind).when(mongoOperationsMock).query(any()); + doReturn(withQueryMock).when(executableFind).as(any()); + doReturn(withQueryMock).when(withQueryMock).matching(any()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyStaticAnnotatedCollation() { + + createQueryForMethod("findWithCollationUsingSpimpleStringValueByFirstName", String.class) // + .execute(new Object[] { "dalinar" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyStaticAnnotatedCollationAsDocument() { + + createQueryForMethod("findWithCollationUsingDocumentByFirstName", String.class) // + .execute(new Object[] { "dalinar" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationAsString() { + + createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) // + .execute(new Object[] { "dalinar", "en_US" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationAsDocument() { + + createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) // + .execute(new Object[] { "dalinar", new Document("locale", "en_US") }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationAsLocale() { + + createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) // + .execute(new Object[] { "dalinar", Locale.US }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1854 + public void shouldThrowExceptionOnNonParsableCollation() { + + createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) // + .execute(new Object[] { "dalinar", 100 }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationIn() { + + createQueryForMethod("findWithCollationUsingPlaceholderInDocumentByFirstName", String.class, String.class) // + .execute(new Object[] { "dalinar", "en_US" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyDynamicAnnotatedCollationWithMultiplePlaceholders() { + + createQueryForMethod("findWithCollationUsingPlaceholdersInDocumentByFirstName", String.class, String.class, + int.class) // + .execute(new Object[] { "dalinar", "en_US", 2 }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").strength(2).toDocument()); + } + + @Test // DATAMONGO-1854 + public void shouldApplyCollationParameter() { + + Collation collation = Collation.of("en_US"); + createQueryForMethod("findWithCollationParameterByFirstName", String.class, Collation.class) // + .execute(new Object[] { "dalinar", collation }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void collationParameterShouldOverrideAnnotation() { + + Collation collation = Collation.of("de_AT"); + createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) // + .execute(new Object[] { "dalinar", collation }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation()).contains(collation); + } + + @Test // DATAMONGO-1854 + public void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() { + + createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) // + .execute(new Object[] { "dalinar", null }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getCollation().map(Collation::toDocument)) + .contains(Collation.of("en_US").toDocument()); + } + + private ReactiveMongoQueryFake createQueryForMethod(String methodName, Class... paramTypes) { + return createQueryForMethod(Repo.class, methodName, paramTypes); + } + + private ReactiveMongoQueryFake createQueryForMethod(Class repository, String methodName, Class... paramTypes) { + + try { + + Method method = repository.getMethod(methodName, paramTypes); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method, + new DefaultRepositoryMetadata(repository), factory, mappingContextMock); + + return new ReactiveMongoQueryFake(queryMethod, mongoOperationsMock); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + private static class ReactiveMongoQueryFake extends AbstractReactiveMongoQuery { + + private boolean isDeleteQuery; + private boolean isLimitingQuery; + + public ReactiveMongoQueryFake(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations) { + super(method, operations, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT); + } + + @Override + protected Query createQuery(ConvertingParameterAccessor accessor) { + return new BasicQuery("{'foo':'bar'}"); + } + + @Override + protected boolean isCountQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected boolean isDeleteQuery() { + return isDeleteQuery; + } + + @Override + protected boolean isLimiting() { + return isLimitingQuery; + } + + public ReactiveMongoQueryFake setDeleteQuery(boolean isDeleteQuery) { + this.isDeleteQuery = isDeleteQuery; + return this; + } + + public ReactiveMongoQueryFake setLimitingQuery(boolean limitingQuery) { + + isLimitingQuery = limitingQuery; + return this; + } + } + + private interface Repo extends ReactiveMongoRepository { + + @org.springframework.data.mongodb.repository.Query(collation = "en_US") + List findWithCollationUsingSpimpleStringValueByFirstName(String firstname); + + @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }") + List findWithCollationUsingDocumentByFirstName(String firstname); + + @org.springframework.data.mongodb.repository.Query(collation = "?1") + List findWithCollationUsingPlaceholderByFirstName(String firstname, Object collation); + + @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1' }") + List findWithCollationUsingPlaceholderInDocumentByFirstName(String firstname, String collation); + + @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1', 'strength' : ?#{[2]}}") + List findWithCollationUsingPlaceholdersInDocumentByFirstName(String firstname, String collation, + int strength); + + List findWithCollationParameterByFirstName(String firstname, Collation collation); + + @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }") + List findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java index c54bbd5062..a39b6a063d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java @@ -15,15 +15,12 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; import java.util.List; import org.bson.Document; -import org.hamcrest.core.IsNull; import org.junit.Test; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; @@ -31,6 +28,7 @@ import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.projection.ProjectionFactory; @@ -82,7 +80,7 @@ public void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuch MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { new Point(10, 20), DISTANCE }); - assertThat(accessor.getFullText(), IsNull.nullValue()); + assertThat(accessor.getFullText()).isNull(); } @Test // DATAMONGO-973 @@ -93,8 +91,8 @@ public void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, Se MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { "spring", TextCriteria.forDefaultLanguage().matching("data") }); - assertThat(accessor.getFullText().getCriteriaObject().toJson(), - equalTo(Document.parse("{ \"$text\" : { \"$search\" : \"data\"}}").toJson())); + assertThat(accessor.getFullText().getCriteriaObject().toJson()) + .isEqualTo(Document.parse("{ \"$text\" : { \"$search\" : \"data\"}}").toJson()); } @Test // DATAMONGO-1110 @@ -111,8 +109,21 @@ public void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, Securi Range range = accessor.getDistanceRange(); - assertThat(range.getLowerBound(), is(Bound.inclusive(min))); - assertThat(range.getUpperBound(), is(Bound.inclusive(max))); + assertThat(range.getLowerBound()).isEqualTo(Bound.inclusive(min)); + assertThat(range.getUpperBound()).isEqualTo(Bound.inclusive(max)); + } + + @Test // DATAMONGO-1854 + public void shouldDetectCollation() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Collation.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); + + Collation collation = Collation.of("en_US"); + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, + new Object[] { "dalinar", collation }); + + assertThat(accessor.getCollation()).isEqualTo(collation); } interface PersonRepository extends Repository { @@ -124,5 +135,8 @@ interface PersonRepository extends Repository { List findByLocationNear(Point point, Range distances); List findByFirstname(String firstname, TextCriteria fullText); + + List findByFirstname(String firstname, Collation collation); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java index 9081dc74ef..9ad082660f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java @@ -15,8 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; import java.util.List; @@ -29,6 +28,7 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.repository.Near; import org.springframework.data.mongodb.repository.Person; @@ -47,17 +47,18 @@ public class MongoParametersUnitTests { @Test public void discoversDistanceParameter() throws NoSuchMethodException, SecurityException { + Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); MongoParameters parameters = new MongoParameters(method, false); - assertThat(parameters.getNumberOfParameters(), is(2)); - assertThat(parameters.getMaxDistanceIndex(), is(1)); - assertThat(parameters.getBindableParameters().getNumberOfParameters(), is(1)); + assertThat(parameters.getNumberOfParameters()).isEqualTo(2); + assertThat(parameters.getMaxDistanceIndex()).isEqualTo(1); + assertThat(parameters.getBindableParameters().getNumberOfParameters()).isOne(); Parameter parameter = parameters.getParameter(1); - assertThat(parameter.isSpecialParameter(), is(true)); - assertThat(parameter.isBindable(), is(false)); + assertThat(parameter.isSpecialParameter()).isTrue(); + assertThat(parameter.isBindable()).isFalse(); } @Test @@ -65,39 +66,44 @@ public void doesNotConsiderPointAsNearForSimpleQuery() throws Exception { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); MongoParameters parameters = new MongoParameters(method, false); - assertThat(parameters.getNearIndex(), is(-1)); + assertThat(parameters.getNearIndex()).isEqualTo(-1); } @Test(expected = IllegalStateException.class) public void rejectsMultiplePointsForGeoNearMethod() throws Exception { + Method method = PersonRepository.class.getMethod("findByLocationNearAndOtherLocation", Point.class, Point.class); new MongoParameters(method, true); } @Test(expected = IllegalStateException.class) public void rejectsMultipleDoubleArraysForGeoNearMethod() throws Exception { + Method method = PersonRepository.class.getMethod("invalidDoubleArrays", double[].class, double[].class); new MongoParameters(method, true); } @Test public void doesNotRejectMultiplePointsForSimpleQueryMethod() throws Exception { + Method method = PersonRepository.class.getMethod("someOtherMethod", Point.class, Point.class); new MongoParameters(method, false); } @Test public void findsAnnotatedPointForGeoNearQuery() throws Exception { + Method method = PersonRepository.class.getMethod("findByOtherLocationAndLocationNear", Point.class, Point.class); MongoParameters parameters = new MongoParameters(method, true); - assertThat(parameters.getNearIndex(), is(1)); + assertThat(parameters.getNearIndex()).isOne(); } @Test public void findsAnnotatedDoubleArrayForGeoNearQuery() throws Exception { + Method method = PersonRepository.class.getMethod("validDoubleArrays", double[].class, double[].class); MongoParameters parameters = new MongoParameters(method, true); - assertThat(parameters.getNearIndex(), is(1)); + assertThat(parameters.getNearIndex()).isOne(); } @Test // DATAMONGO-973 @@ -105,7 +111,7 @@ public void shouldFindTextCriteriaAtItsIndex() throws SecurityException, NoSuchM Method method = PersonRepository.class.getMethod("findByNameAndText", String.class, TextCriteria.class); MongoParameters parameters = new MongoParameters(method, false); - assertThat(parameters.getFullTextParameterIndex(), is(1)); + assertThat(parameters.getFullTextParameterIndex()).isOne(); } @Test // DATAMONGO-973 @@ -113,7 +119,7 @@ public void shouldTreatTextCriteriaParameterAsSpecialParameter() throws Security Method method = PersonRepository.class.getMethod("findByNameAndText", String.class, TextCriteria.class); MongoParameters parameters = new MongoParameters(method, false); - assertThat(parameters.getParameter(parameters.getFullTextParameterIndex()).isSpecialParameter(), is(true)); + assertThat(parameters.getParameter(parameters.getFullTextParameterIndex()).isSpecialParameter()).isTrue(); } @Test // DATAMONGO-1110 @@ -122,19 +128,37 @@ public void shouldFindMinAndMaxDistanceParameters() throws NoSuchMethodException Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Range.class); MongoParameters parameters = new MongoParameters(method, false); - assertThat(parameters.getRangeIndex(), is(1)); - assertThat(parameters.getMaxDistanceIndex(), is(-1)); + assertThat(parameters.getRangeIndex()).isOne(); + assertThat(parameters.getMaxDistanceIndex()).isEqualTo(-1); } @Test // DATAMONGO-1110 - public void shouldNotHaveMinDistanceIfOnlyOneDistanceParameterPresent() throws NoSuchMethodException, - SecurityException { + public void shouldNotHaveMinDistanceIfOnlyOneDistanceParameterPresent() + throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); + MongoParameters parameters = new MongoParameters(method, false); + + assertThat(parameters.getRangeIndex()).isEqualTo(-1); + assertThat(parameters.getMaxDistanceIndex()).isOne(); + } + + @Test // DATAMONGO-1854 + public void shouldReturnMinusOneIfCollationParameterDoesNotExist() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); MongoParameters parameters = new MongoParameters(method, false); - assertThat(parameters.getRangeIndex(), is(-1)); - assertThat(parameters.getMaxDistanceIndex(), is(1)); + assertThat(parameters.getCollationParameterIndex()).isEqualTo(-1); + } + + @Test // DATAMONGO-1854 + public void shouldReturnIndexOfCollationParameterIfExists() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findByText", String.class, Collation.class); + MongoParameters parameters = new MongoParameters(method, false); + + assertThat(parameters.getCollationParameterIndex()).isOne(); } interface PersonRepository { @@ -154,5 +178,7 @@ interface PersonRepository { List findByNameAndText(String name, TextCriteria text); List findByLocationNear(Point point, Range range); + + List findByText(String text, Collation collation); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java index 25874033c4..62111a3d9c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -27,6 +27,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; + import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.geo.Distance; @@ -53,6 +54,8 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.ReflectionUtils; /** @@ -72,6 +75,7 @@ public class MongoQueryExecutionUnitTests { @Mock TerminatingFindNear terminatingGeoMock; @Mock DbRefResolver dbRefResolver; + SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); Point POINT = new Point(10, 20); Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS); RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); @@ -140,7 +144,8 @@ public void pagingGeoExecutionShouldUseCountFromResultWithOffsetAndResultsWithin ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(0, 10) })); - PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock); + PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, EXPRESSION_PARSER, + QueryMethodEvaluationContextProvider.DEFAULT); PagingGeoNearExecution execution = new PagingGeoNearExecution(findOperationMock, queryMethod, accessor, query); execution.execute(new Query()); @@ -158,7 +163,8 @@ public void pagingGeoExecutionRetrievesObjectsForPageableOutOfRange() { ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(2, 10) })); - PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock); + PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, EXPRESSION_PARSER, + QueryMethodEvaluationContextProvider.DEFAULT); PagingGeoNearExecution execution = new PagingGeoNearExecution(findOperationMock, queryMethod, accessor, query); execution.execute(new Query()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java index 6fad108a87..5759fcce43 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java @@ -33,6 +33,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; + import org.springframework.beans.factory.annotation.Value; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; @@ -51,6 +52,8 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * Unit tests for {@link PartTreeMongoQuery}. @@ -217,7 +220,8 @@ private PartTreeMongoQuery createQueryForMethod(String methodName, Class... p MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(Repo.class), factory, mappingContext); - return new PartTreeMongoQuery(queryMethod, mongoOperationsMock); + return new PartTreeMongoQuery(queryMethod, mongoOperationsMock, new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT); } catch (Exception e) { throw new IllegalArgumentException(e.getMessage(), e); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index 28ebd223de..5672c2e91b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -26,8 +26,10 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.lang.Nullable; /** * Simple {@link ParameterAccessor} that returns the given parameters unfiltered. @@ -40,6 +42,7 @@ class StubParameterAccessor implements MongoParameterAccessor { private final Object[] values; private Range range = Range.unbounded(); + private @Nullable Collation colllation; /** * Creates a new {@link ConvertingParameterAccessor} backed by a {@link StubParameterAccessor} simply returning the @@ -63,6 +66,8 @@ public StubParameterAccessor(Object... values) { this.range = (Range) value; } else if (value instanceof Distance) { this.range = Range.from(Bound. unbounded()).to(Bound.inclusive((Distance) value)); + } else if (value instanceof Collation) { + this.colllation = Collation.class.cast(value); } } } @@ -133,6 +138,15 @@ public TextCriteria getFullText() { return null; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation() + */ + @Override + public Collation getCollation() { + return this.colllation; + } + /* (non-Javadoc) * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues() */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java index 7f4b5205f1..e5ad2c718f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java @@ -15,18 +15,28 @@ */ package org.springframework.data.mongodb.repository.support; -import static org.mockito.ArgumentMatchers.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.mockito.Mockito.anyString; +import org.bson.Document; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.index.IndexDefinition; +import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.index.IndexOperationsProvider; +import org.springframework.data.mongodb.repository.query.MongoEntityMetadata; +import org.springframework.data.mongodb.repository.query.MongoQueryMethod; import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.util.Streamable; /** * Unit tests for {@link IndexEnsuringQueryCreationListener}. @@ -39,26 +49,109 @@ public class IndexEnsuringQueryCreationListenerUnitTests { IndexEnsuringQueryCreationListener listener; @Mock IndexOperationsProvider provider; + @Mock PartTree partTree; + @Mock PartTreeMongoQuery partTreeQuery; + @Mock MongoQueryMethod queryMethod; + @Mock IndexOperations indexOperations; + @Mock MongoEntityMetadata entityInformation; @Before public void setUp() { + this.listener = new IndexEnsuringQueryCreationListener(provider); + + partTreeQuery = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS); + when(partTreeQuery.getTree()).thenReturn(partTree); + when(provider.indexOps(anyString())).thenReturn(indexOperations); + when(queryMethod.getEntityInformation()).thenReturn(entityInformation); + when(entityInformation.getCollectionName()).thenReturn("persons"); } @Test // DATAMONGO-1753 public void skipsQueryCreationForMethodWithoutPredicate() { - PartTree tree = mock(PartTree.class); - when(tree.hasPredicate()).thenReturn(false); + when(partTree.hasPredicate()).thenReturn(false); - PartTreeMongoQuery query = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS); - when(query.getTree()).thenReturn(tree); - - listener.onCreation(query); + listener.onCreation(partTreeQuery); verify(provider, times(0)).indexOps(any()); } + @Test // DATAMONGO-1854 + public void usesCollationWhenPresentAndFixedValue() { + + when(partTree.hasPredicate()).thenReturn(true); + when(partTree.getParts()).thenReturn(Streamable.empty()); + when(partTree.getSort()).thenReturn(Sort.unsorted()); + when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod); + when(queryMethod.hasAnnotatedCollation()).thenReturn(true); + when(queryMethod.getAnnotatedCollation()).thenReturn("en_US"); + + listener.onCreation(partTreeQuery); + + ArgumentCaptor indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class); + verify(indexOperations).ensureIndex(indexArgumentCaptor.capture()); + + IndexDefinition indexDefinition = indexArgumentCaptor.getValue(); + assertThat(indexDefinition.getIndexOptions()).isEqualTo(new Document("collation", new Document("locale", "en_US"))); + } + + @Test // DATAMONGO-1854 + public void usesCollationWhenPresentAndFixedDocumentValue() { + + when(partTree.hasPredicate()).thenReturn(true); + when(partTree.getParts()).thenReturn(Streamable.empty()); + when(partTree.getSort()).thenReturn(Sort.unsorted()); + when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod); + when(queryMethod.hasAnnotatedCollation()).thenReturn(true); + when(queryMethod.getAnnotatedCollation()).thenReturn("{ 'locale' : 'en_US' }"); + + listener.onCreation(partTreeQuery); + + ArgumentCaptor indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class); + verify(indexOperations).ensureIndex(indexArgumentCaptor.capture()); + + IndexDefinition indexDefinition = indexArgumentCaptor.getValue(); + assertThat(indexDefinition.getIndexOptions()).isEqualTo(new Document("collation", new Document("locale", "en_US"))); + } + + @Test // DATAMONGO-1854 + public void skipsCollationWhenPresentButDynamic() { + + when(partTree.hasPredicate()).thenReturn(true); + when(partTree.getParts()).thenReturn(Streamable.empty()); + when(partTree.getSort()).thenReturn(Sort.unsorted()); + when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod); + when(queryMethod.hasAnnotatedCollation()).thenReturn(true); + when(queryMethod.getAnnotatedCollation()).thenReturn("{ 'locale' : '?0' }"); + + listener.onCreation(partTreeQuery); + + ArgumentCaptor indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class); + verify(indexOperations).ensureIndex(indexArgumentCaptor.capture()); + + IndexDefinition indexDefinition = indexArgumentCaptor.getValue(); + assertThat(indexDefinition.getIndexOptions()).isEmpty(); + } + + @Test // DATAMONGO-1854 + public void skipsCollationWhenNotPresent() { + + when(partTree.hasPredicate()).thenReturn(true); + when(partTree.getParts()).thenReturn(Streamable.empty()); + when(partTree.getSort()).thenReturn(Sort.unsorted()); + when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod); + when(queryMethod.hasAnnotatedCollation()).thenReturn(false); + + listener.onCreation(partTreeQuery); + + ArgumentCaptor indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class); + verify(indexOperations).ensureIndex(indexArgumentCaptor.capture()); + + IndexDefinition indexDefinition = indexArgumentCaptor.getValue(); + assertThat(indexDefinition.getIndexOptions()).isEmpty(); + } + interface SampleRepository { Object findAllBy(); 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 0089432663..8eee93ee0c 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.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 String getCollectionName() { 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 0000000000..1a5feb0ca9 --- /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 0000000000..64e34255e5 --- /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 1a7913c8b0..c168d27f61 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,6 +1746,225 @@ 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 `collation` attribute of the `@Query` annotation. + +.Collation support for Repositories +==== +[source,java] +---- +public interface PersonRepository extends MongoRepository { + + @Query(collation = "en_US") <1> + List findByFirstname(String firstname); + + @Query(collation = "{ 'locale' : 'en_US' }") <2> + List findPersonByFirstname(String firstname); + + @Query(collation = "?1") <3> + List findByFirstname(String firstname, Object collation); + + @Query(collation = "{ 'locale' : '?1' }") <4> + List findByFirstname(String firstname, String collation); + + List findByFirstname(String firstname, Collation collation); <5> + + @Query(collation = "{ 'locale' : 'en_US' }") + List findByFirstname(String firstname, @Nullable Collation collation); <6> +} +---- +<1> Static collation definition resulting in `{ 'locale' : 'en_US' }`. +<2> Static collation definition resulting in `{ 'locale' : 'en_US' }`. +<3> Dynamic collation depending on 2nd method argument. Allowed types include `String` (eg. 'en_US'), `Locacle` (eg. Locacle.US) +and `Document` (eg. new Document("locale", "en_US")) +<4> Dynamic collation depending on 2nd method argument. +<5> Apply the `Collation` method parameter to the query. +<6> The `Collation` method parameter overrides the default `collation` from `@Query` if not null. + +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]] +=== JSON Schema + +As of version 3.6, MongoDB supports collections that validate documents against a provided https://docs.mongodb.com/manual/core/schema-validation/#json-schema[JSON Schema]. +The schema itself and both validation action and level can be defined when creating the collection, as the following example shows: + +.Sample JSON schema +==== +[source,json] +---- +{ + "type": "object", <1> + + "required": [ "firstname", "lastname" ], <2> + + "properties": { <3> + + "firstname": { <4> + "type": "string", + "enum": [ "luke", "han" ] + }, + "address": { <5> + "type": "object", + "properties": { + "postCode": { "type": "string", "minLength": 4, "maxLength": 5 } + } + } + } +} +---- +<1> JSON schema documents always describe a whole document from its root. A schema is a schema object itself that can contain +embedded schema objects that describe properties and subdocuments. +<2> `required` is a property that describes which properties are required in a document. It can be specified optionally, along with other +schema constraints. See MongoDB's documentation on https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#available-keywords[available keywords]. +<3> `properties` is related to a schema object that describes an `object` type. It contains property-specific schema constraints. +<4> `firstname` specifies constraints for the `firsname` field inside the document. Here, it is a string-based `properties` element declaring + possible field values. +<5> `address` is a subdocument defining a schema for values in its `postCode` field. +==== + +You can provide a schema either by specifying a schema document (that is, by using the `Document` API to parse or build a document object) or by building it with Spring Data's JSON schema utilities in `org.springframework.data.mongodb.core.schema`. `MongoJsonSchema` is the entry point for all JSON schema-related operations. The following example shows how use `MongoJsonSchema.builder()` to create a JSON schema: + +.Creating a JSON schema +==== +[source,java] +---- +MongoJsonSchema.builder() <1> + .required("firstname", "lastname") <2> + + .properties( + string("firstname").possibleValues("luke", "han"), <3> + + object("address") + .properties(string("postCode").minLength(4).maxLength(5))) + + .build(); <4> +---- +<1> Obtain a schema builder to configure the schema with a fluent API. +<2> Configure required properties. +<3> Configure the String-typed `firstname` field, allowing only `luke` and `han` values. Properties can be typed or untyped. Use a static import of `JsonSchemaProperty` to make the syntax slightly more compact and to get entry points such as `string(…)`. +<4> Build the schema object. Use the schema to create either a collection or <>. +==== + +There are already some predefined and strongly typed schema objects (`JsonSchemaObject` and `JsonSchemaProperty`) available +through static methods on the gateway interfaces. +However, you may need to build custom property validation rules, which can be created through the builder API, as the following example shows: + +[source,java] +---- +// "birthdate" : { "bsonType": "date" } +JsonSchemaProperty.named("birthdate").ofType(Type.dateType()); + +// "birthdate" : { "bsonType": "date", "description", "Must be a date" } +JsonSchemaProperty.named("birthdate").with(JsonSchemaObject.of(Type.dateType()).description("Must be a date")); +---- + +`CollectionOptions` provides the entry point to schema support for collections, as the following example shows: + +.Create collection with `$jsonSchema` +==== +[source,java] +---- +MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build(); + +template.createCollection(Person.class, CollectionOptions.empty().schema(schema)); +---- +==== + +You can use a schema to query any collection for documents that match a given structure defined by a JSON schema, as the following example shows: + +.Query for Documents matching a `$jsonSchema` +==== +[source,java] +---- +MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build(); + +template.find(query(matchingDocumentStructure(schema)), Person.class); +---- +==== + +The following table shows the supported JSON schema types: + +[cols="3,1,6", options="header"] +.Supported JSON schema types +|=== +| Schema Type +| Java Type +| Schema Properties + +| `untyped` +| - +| `description`, generated `description`, `enum`, `allOf`, `anyOf`, `oneOf`, `not` + +| `object` +| `Object` +| `required`, `additionalProperties`, `properties`, `minProperties`, `maxProperties`, `patternProperties` + +| `array` +| any array except `byte[]` +| `uniqueItems`, `additionalItems`, `items`, `minItems`, `maxItems` + +| `string` +| `String` +| `minLength`, `maxLentgth`, `pattern` + +| `int` +| `int`, `Integer` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `long` +| `long`, `Long` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `double` +| `float`, `Float`, `double`, `Double` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `decimal` +| `BigDecimal` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `number` +| `Number` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `binData` +| `byte[]` +| (none) + +| `boolean` +| `boolean`, `Boolean` +| (none) + +| `null` +| `null` +| (none) + +| `objectId` +| `ObjectId` +| (none) + +| `date` +| `java.util.Date` +| (none) + +| `timestamp` +| `BsonTimestamp` +| (none) + +| `regex` +| `java.util.regex.Pattern` +| (none) + +|=== + +NOTE: `untyped` is a generic type that is inherited by all typed schema types. It provides all `untyped` schema properties to typed schema types. + +For more information, see https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#op._S_jsonSchema[$jsonSchema]. + [[mongo.query.fluent-template-api]] === Fluent Template API