From 0d752fd6e65826ea3712ea354b19407c19d3ddcd Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 23 Aug 2022 09:04:35 +0200 Subject: [PATCH] Introduce dedicated Collation annotation. The Collation annotation mainly serves as a meta annotation that allows common access to retrieving collation values for annotated queries, aggregations, etc. Original Pull Request: #4131 --- .../mongodb/core/annotation/Collation.java | 44 +++++++++++ .../mongodb/core/annotation/package-info.java | 6 ++ .../mongodb/core/index/CompoundIndex.java | 5 ++ .../data/mongodb/core/index/Indexed.java | 5 ++ .../MongoPersistentEntityIndexResolver.java | 69 +++++++++-------- .../mongodb/core/index/WildcardIndexed.java | 5 ++ .../data/mongodb/core/mapping/Document.java | 3 + .../data/mongodb/repository/Aggregation.java | 3 + .../data/mongodb/repository/Query.java | 4 + .../repository/query/MongoQueryMethod.java | 17 ++--- .../mongodb/util/spel/ExpressionUtils.java | 14 ++++ ...ersistentEntityIndexResolverUnitTests.java | 76 ++++++++++++++++++- .../query/MongoQueryMethodUnitTests.java | 39 ++++++++++ .../ReactiveMongoQueryMethodUnitTests.java | 51 +++++++++++-- src/main/asciidoc/reference/mongodb.adoc | 30 +++++++- 15 files changed, 317 insertions(+), 54 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/Collation.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/Collation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/Collation.java new file mode 100644 index 000000000..e73bb69ea --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/Collation.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@link Collation} allows to define the rules used for language-specific string comparison. + * + * @see https://www.mongodb.com/docs/manual/reference/collation/ + * @author Christoph Strobl + * @since 4.0 + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface Collation { + + /** + * The actual collation definition in JSON format or a + * {@link org.springframework.expression.spel.standard.SpelExpression template expression} resolving to either a JSON + * String or a {@link org.bson.Document}. The keys of the JSON document are configuration options for the collation. + * + * @return an empty {@link String} by default. + */ + String value() default ""; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java new file mode 100644 index 000000000..3e08dc101 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java @@ -0,0 +1,6 @@ +/** + * Core Spring Data MongoDB annotations not limited to a special use case (like Query,...). + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.mongodb.core.annotation; + diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java index da2bbfde5..748711f7a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java @@ -22,7 +22,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.mongodb.core.annotation.Collation; import org.springframework.data.mongodb.core.mapping.Document; + /** * Mark a class to use compound indexes.
*

@@ -49,6 +52,7 @@ import org.springframework.data.mongodb.core.mapping.Document; * @author Dave Perryman * @author Stefan Tirea */ +@Collation @Target({ ElementType.TYPE }) @Documented @Repeatable(CompoundIndexes.class) @@ -181,5 +185,6 @@ public @interface CompoundIndex { * "https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/ * @since 4.0 */ + @AliasFor(annotation = Collation.class, attribute = "value") String collation() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java index bc94563a6..5a21ddb41 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java @@ -20,7 +20,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.mongodb.core.annotation.Collation; import org.springframework.data.mongodb.core.mapping.Document; + /** * Mark a field to be indexed using MongoDB's indexing feature. * @@ -34,6 +37,7 @@ import org.springframework.data.mongodb.core.mapping.Document; * @author Mark Paluch * @author Stefan Tirea */ +@Collation @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface Indexed { @@ -188,5 +192,6 @@ public @interface Indexed { * @see https://www.mongodb.com/docs/manual/reference/collation/ * @since 4.0 */ + @AliasFor(annotation = Collation.class, attribute = "value") String collation() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index dc60253d9..5853c4e57 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.index; +import java.lang.annotation.Annotation; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -23,13 +24,15 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.Association; @@ -50,12 +53,10 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.DotPath; +import org.springframework.data.mongodb.util.spel.ExpressionUtils; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.ParserContext; -import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -454,10 +455,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity)); } - if (StringUtils.hasText(index.collation())) { - indexDefinition.collation(evaluateCollation(index.collation(), entity)); - } - + indexDefinition.collation(resolveCollation(index, entity)); return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } @@ -478,12 +476,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity)); } - if (StringUtils.hasText(index.collation())) { - indexDefinition.collation(evaluateCollation(index.collation(), entity)); - } else if (entity != null && entity.hasCollation()) { - indexDefinition.collation(entity.getCollation()); - } - + indexDefinition.collation(resolveCollation(index, entity)); return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } @@ -498,7 +491,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { return new org.bson.Document(dotPath, 1); } - Object keyDefToUse = evaluate(keyDefinitionString, getEvaluationContextForProperty(entity)); + Object keyDefToUse = ExpressionUtils.evaluate(keyDefinitionString, () -> getEvaluationContextForProperty(entity)); org.bson.Document dbo = (keyDefToUse instanceof org.bson.Document) ? (org.bson.Document) keyDefToUse : org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse)); @@ -567,7 +560,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { } Duration timeout = computeIndexTimeout(index.expireAfter(), - getEvaluationContextForProperty(persistentProperty.getOwner())); + () -> getEvaluationContextForProperty(persistentProperty.getOwner())); if (!timeout.isZero() && !timeout.isNegative()) { indexDefinition.expire(timeout); } @@ -577,16 +570,13 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), persistentProperty.getOwner())); } - if (StringUtils.hasText(index.collation())) { - indexDefinition.collation(evaluateCollation(index.collation(), persistentProperty.getOwner())); - } - + indexDefinition.collation(resolveCollation(index, persistentProperty.getOwner())); return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity entity) { - Object result = evaluate(filterExpression, getEvaluationContextForProperty(entity)); + Object result = ExpressionUtils.evaluate(filterExpression, () -> getEvaluationContextForProperty(entity)); if (result instanceof org.bson.Document) { return PartialIndexFilter.of((org.bson.Document) result); @@ -597,7 +587,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { private org.bson.Document evaluateWildcardProjection(String projectionExpression, PersistentEntity entity) { - Object result = evaluate(projectionExpression, getEvaluationContextForProperty(entity)); + Object result = ExpressionUtils.evaluate(projectionExpression, () -> getEvaluationContextForProperty(entity)); if (result instanceof org.bson.Document) { return (org.bson.Document) result; @@ -608,7 +598,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { private Collation evaluateCollation(String collationExpression, PersistentEntity entity) { - Object result = evaluate(collationExpression, getEvaluationContextForProperty(entity)); + Object result = ExpressionUtils.evaluate(collationExpression, () -> getEvaluationContextForProperty(entity)); if (result instanceof org.bson.Document) { return Collation.from((org.bson.Document) result); } @@ -618,6 +608,9 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { if (result instanceof String) { return Collation.parse(result.toString()); } + if (result instanceof Map) { + return Collation.from(new org.bson.Document((Map) result)); + } throw new IllegalStateException("Cannot parse collation " + result); } @@ -726,7 +719,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { String nameToUse = ""; if (StringUtils.hasText(indexName)) { - Object result = evaluate(indexName, getEvaluationContextForProperty(entity)); + Object result = ExpressionUtils.evaluate(indexName, () -> getEvaluationContextForProperty(entity)); if (result != null) { nameToUse = ObjectUtils.nullSafeToString(result); @@ -787,9 +780,9 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { * @since 2.2 * @throws IllegalArgumentException for invalid duration values. */ - private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) { + private static Duration computeIndexTimeout(String timeoutValue, Supplier evaluationContext) { - Object evaluatedTimeout = evaluate(timeoutValue, evaluationContext); + Object evaluatedTimeout = ExpressionUtils.evaluate(timeoutValue, evaluationContext); if (evaluatedTimeout == null) { return Duration.ZERO; @@ -808,15 +801,25 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { return DurationStyle.detectAndParse(val); } + /** + * Resolve the "collation" attribute from a given {@link Annotation} if present. + * + * @param annotation + * @param entity + * @return the collation present on either the annotation or the entity as a fallback. Might be {@literal null}. + * @since 4.0 + */ @Nullable - private static Object evaluate(String value, EvaluationContext evaluationContext) { + private Collation resolveCollation(Annotation annotation, @Nullable PersistentEntity entity) { + return MergedAnnotation.from(annotation).getValue("collation", String.class).filter(StringUtils::hasText) + .map(it -> evaluateCollation(it, entity)).orElseGet(() -> { - Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION); - if (expression instanceof LiteralExpression) { - return value; - } - - return expression.getValue(evaluationContext, Object.class); + if (entity instanceof MongoPersistentEntity mongoPersistentEntity + && mongoPersistentEntity.hasCollation()) { + return mongoPersistentEntity.getCollation(); + } + return null; + }); } private static boolean isMapWithoutWildcardIndex(MongoPersistentProperty property) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java index 85cc815a3..63381e157 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java @@ -21,6 +21,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.mongodb.core.annotation.Collation; + /** * Annotation for an entity or property that should be used as key for a * Wildcard Index.
@@ -79,6 +82,7 @@ import java.lang.annotation.Target; * @author Christoph Strobl * @since 3.3 */ +@Collation @Documented @Target({ ElementType.TYPE, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @@ -126,5 +130,6 @@ public @interface WildcardIndexed { * * @return an empty {@link String} by default. */ + @AliasFor(annotation = Collation.class, attribute = "value") String collation() default ""; } 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 d66ee21f9..e88a76307 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 @@ -23,6 +23,7 @@ import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.Persistent; +import org.springframework.data.mongodb.core.annotation.Collation; /** * Identifies a domain object to be persisted to MongoDB. @@ -32,6 +33,7 @@ import org.springframework.data.annotation.Persistent; * @author Christoph Strobl */ @Persistent +@Collation @Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) @@ -71,6 +73,7 @@ public @interface Document { * @return an empty {@link String} by default. * @since 2.2 */ + @AliasFor(annotation = Collation.class, attribute = "value") String collation() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java index e8d70e5b0..244d55220 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java @@ -23,6 +23,7 @@ import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.mongodb.core.annotation.Collation; /** * The {@link Aggregation} annotation can be used to annotate a {@link org.springframework.data.repository.Repository} @@ -38,6 +39,7 @@ import org.springframework.data.annotation.QueryAnnotation; * @author Christoph Strobl * @since 2.2 */ +@Collation @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Documented @@ -123,5 +125,6 @@ public @interface Aggregation { * * @return an empty {@link String} by default. */ + @AliasFor(annotation = Collation.class, attribute = "value") String collation() default ""; } 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 2f2b62df8..99fc16687 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 @@ -21,7 +21,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.mongodb.core.annotation.Collation; /** * Annotation to declare finder queries directly on repository methods. Both attributes allow using a placeholder @@ -32,6 +34,7 @@ import org.springframework.data.annotation.QueryAnnotation; * @author Christoph Strobl * @author Mark Paluch */ +@Collation @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Documented @@ -124,5 +127,6 @@ public @interface Query { * @return an empty {@link String} by default. * @since 2.2 */ + @AliasFor(annotation = Collation.class, attribute = "value") String collation() default ""; } 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 ebc2d6cdf..2a2e9ff75 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 @@ -28,6 +28,7 @@ import org.springframework.data.geo.GeoPage; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.annotation.Collation; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -321,14 +322,7 @@ public class MongoQueryMethod extends QueryMethod { * @since 2.2 */ public boolean hasAnnotatedCollation() { - - Optional optionalCollation = lookupQueryAnnotation().map(Query::collation); - - if (!optionalCollation.isPresent()) { - optionalCollation = lookupAggregationAnnotation().map(Aggregation::collation); - } - - return optionalCollation.filter(StringUtils::hasText).isPresent(); + return doFindAnnotation(Collation.class).map(Collation::value).filter(StringUtils::hasText).isPresent(); } /** @@ -341,10 +335,9 @@ public class MongoQueryMethod extends QueryMethod { */ public String getAnnotatedCollation() { - return lookupQueryAnnotation().map(Query::collation) - .orElseGet(() -> lookupAggregationAnnotation().map(Aggregation::collation) // + return doFindAnnotation(Collation.class).map(Collation::value) // .orElseThrow(() -> new IllegalStateException( - "Expected to find @Query annotation but did not; Make sure to check hasAnnotatedCollation() before."))); + "Expected to find @Collation annotation but did not; Make sure to check hasAnnotatedCollation() before.")); } /** @@ -447,7 +440,7 @@ public class MongoQueryMethod extends QueryMethod { private boolean isNumericOrVoidReturnValue() { Class resultType = getReturnedObjectType(); - if(ReactiveWrappers.usesReactiveType(resultType)) { + if (ReactiveWrappers.usesReactiveType(resultType)) { resultType = getReturnType().getComponentType().getType(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java index 8215d5ecc..af896c2a0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java @@ -15,6 +15,9 @@ */ package org.springframework.data.mongodb.util.spel; +import java.util.function.Supplier; + +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.common.LiteralExpression; @@ -49,4 +52,15 @@ public final class ExpressionUtils { Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION); return expression instanceof LiteralExpression ? null : expression; } + + @Nullable + public static Object evaluate(String value, Supplier evaluationContext) { + + Expression expression = detectExpression(value); + if (expression == null) { + return value; + } + + return expression.getValue(evaluationContext.get(), Object.class); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index 2cbd2436d..6eb22198d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -713,6 +713,32 @@ public class MongoPersistentEntityIndexResolverUnitTests { assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1)); } + @Test // GH-3002 + public void compoundIndexWithCollationFromDocumentAnnotation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithCompoundCollationFromDocument.class); + + IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); + assertThat(indexDefinition.getIndexOptions()) + .isEqualTo(new org.bson.Document().append("name", "compound_index_with_collation").append("collation", + new org.bson.Document().append("locale", "en_US").append("strength", 2))); + assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1)); + } + + @Test // GH-3002 + public void compoundIndexWithEvaluatedCollationFromAnnotation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithEvaluatedCollationFromCompoundIndex.class); + + IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); + assertThat(indexDefinition.getIndexOptions()) + .isEqualTo(new org.bson.Document().append("name", "compound_index_with_collation").append("collation", + new org.bson.Document().append("locale", "de_AT"))); + assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1)); + } + @Document("CompoundIndexOnLevelOne") class CompoundIndexOnLevelOne { @@ -793,6 +819,14 @@ public class MongoPersistentEntityIndexResolverUnitTests { @CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}", collation = "{'locale': 'en_US', 'strength': 2}") class CompoundIndexWithCollation {} + + @Document(collation = "{'locale': 'en_US', 'strength': 2}") + @CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}") + class WithCompoundCollationFromDocument {} + + @Document(collation = "{'locale': 'en_US', 'strength': 2}") + @CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}", collation = "#{{ 'locale' : 'de' + '_' + 'AT' }}") + class WithEvaluatedCollationFromCompoundIndex {} } public static class TextIndexedResolutionTests { @@ -1423,7 +1457,7 @@ public class MongoPersistentEntityIndexResolverUnitTests { public void indexedWithCollation() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( - IndexedWithCollation.class); + WithCollationFromIndexedAnnotation.class); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value") @@ -1431,6 +1465,29 @@ public class MongoPersistentEntityIndexResolverUnitTests { .append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2))); } + @Test // GH-3002 + public void indexedWithCollationFromDocumentAnnotation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithCollationFromDocumentAnnotation.class); + + IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); + assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value") + .append("unique", true) + .append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2))); + } + + @Test // GH-3002 + public void indexedWithEvaluatedCollation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithEvaluatedCollationFromIndexedAnnotation.class); + + IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); + assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value") + .append("collation", new org.bson.Document().append("locale", "de_AT"))); + } + @Document class MixedIndexRoot { @@ -1749,11 +1806,26 @@ public class MongoPersistentEntityIndexResolverUnitTests { } @Document - class IndexedWithCollation { + class WithCollationFromIndexedAnnotation { + @Indexed(collation = "{'locale': 'en_US', 'strength': 2}", unique = true) // private String value; } + @Document(collation = "{'locale': 'en_US', 'strength': 2}") + class WithCollationFromDocumentAnnotation { + + @Indexed(unique = true) // + private String value; + } + + @Document(collation = "en_US") + class WithEvaluatedCollationFromIndexedAnnotation { + + @Indexed(collation = "#{{'locale' : 'de' + '_' + 'AT'}}") // + private String value; + } + @HashIndexed @Indexed @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java index 3d5a55250..270cef35e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java @@ -31,6 +31,7 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.User; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.annotation.Collation; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -39,6 +40,7 @@ import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Contact; import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.Person; +import org.springframework.data.mongodb.repository.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; @@ -278,6 +280,33 @@ public class MongoQueryMethodUnitTests { .withMessageContaining("findAndIncrementVisitsByFirstname"); } + @Test // GH-3002 + void readsCollationFromAtCollationAnnotation() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtCollationByFirstname", String.class); + + assertThat(method.hasAnnotatedCollation()).isTrue(); + assertThat(method.getAnnotatedCollation()).isEqualTo("en_US"); + } + + @Test // GH-3002 + void readsCollationFromAtQueryAnnotation() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtQueryByFirstname", String.class); + + assertThat(method.hasAnnotatedCollation()).isTrue(); + assertThat(method.getAnnotatedCollation()).isEqualTo("en_US"); + } + + @Test // GH-3002 + void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class); + + assertThat(method.hasAnnotatedCollation()).isTrue(); + assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); + } + private MongoQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { Method method = repository.getMethod(name, parameters); @@ -338,6 +367,16 @@ public class MongoQueryMethodUnitTests { void findAndUpdateBy(String firstname, UpdateDefinition update); void findAndUpdateBy(String firstname, AggregationUpdate update); + + @Collation("en_US") + List findWithCollationFromAtCollationByFirstname(String firstname); + + @Query(collation = "en_US") + List findWithCollationFromAtQueryByFirstname(String firstname); + + @Collation("de_AT") + @Query(collation = "en_US") + List findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname); } interface SampleRepository extends Repository { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java index ff2c217e6..7334a1ddd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java @@ -17,6 +17,10 @@ package org.springframework.data.mongodb.repository.query; import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.annotation.Collation; +import org.springframework.data.mongodb.repository.Query; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -24,8 +28,6 @@ import java.lang.reflect.Method; import java.util.List; import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Page; @@ -56,7 +58,7 @@ public class ReactiveMongoQueryMethodUnitTests { MongoMappingContext context; - @Before + @BeforeEach public void setUp() { context = new MongoMappingContext(); } @@ -102,13 +104,13 @@ public class ReactiveMongoQueryMethodUnitTests { .isTrue(); } - @Test(expected = IllegalArgumentException.class) // DATAMONGO-1444 + @Test // DATAMONGO-1444 public void rejectsNullMappingContext() throws Exception { Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Point.class); - new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), - new SpelAwareProxyProjectionFactory(), null); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), + new SpelAwareProxyProjectionFactory(), null)); } @Test // DATAMONGO-1444 @@ -197,6 +199,33 @@ public class ReactiveMongoQueryMethodUnitTests { .withMessageContaining("findAndIncrementVisitsByFirstname"); } + @Test // GH-3002 + void readsCollationFromAtCollationAnnotation() throws Exception { + + ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithCollationFromAtCollationByFirstname", String.class); + + assertThat(method.hasAnnotatedCollation()).isTrue(); + assertThat(method.getAnnotatedCollation()).isEqualTo("en_US"); + } + + @Test // GH-3002 + void readsCollationFromAtQueryAnnotation() throws Exception { + + ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithCollationFromAtQueryByFirstname", String.class); + + assertThat(method.hasAnnotatedCollation()).isTrue(); + assertThat(method.getAnnotatedCollation()).isEqualTo("en_US"); + } + + @Test // GH-3002 + void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception { + + ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class); + + assertThat(method.hasAnnotatedCollation()).isTrue(); + assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); + } + private ReactiveMongoQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { @@ -238,6 +267,16 @@ public class ReactiveMongoQueryMethodUnitTests { @Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }", collation = "de_AT") Flux findByAggregationWithCollation(); + + @Collation("en_US") + List findWithCollationFromAtCollationByFirstname(String firstname); + + @Query(collation = "en_US") + List findWithCollationFromAtQueryByFirstname(String firstname); + + @Collation("de_AT") + @Query(collation = "en_US") + List findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname); } interface SampleRepository extends Repository { diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index ea545122a..b5a5f795a 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -2011,7 +2011,35 @@ and `Document` (eg. new Document("locale", "en_US")) NOTE: In case you enabled the automatic index creation for repository finder methods a potential static collation definition, as shown in (1) and (2), will be included when creating the index. -TIP: The most specifc `Collation` outroules potentially defined others. Which means Method argument over query method annotation over doamin type annotation. +TIP: The most specifc `Collation` outrules potentially defined others. Which means Method argument over query method annotation over domain type annotation. +==== + +To streamline usage of collation attributes throughout the codebase it is also possible to use the `@Collation` annotation, which serves as a meta annotation for the ones mentioned above. +The same rules and locations apply, plus, direct usage of `@Collation` supersedes any collation values defined on `@Query` and other annotations. +Which means, if a collation is declared via `@Query` and additionally via `@Collation`, then the one from `@Collation` is picked. + +.Using `@Collation` +==== +[source,java] +---- +@Collation("en_US") <1> +class Game { + // ... +} + +interface GameRepository extends Repository { + + @Collation("en_GB") <2> + List findByTitle(String title); + + @Collation("de_AT") <3> + @Query(collation="en_GB") + List findByDescriptionContaining(String keyword); +} +---- +<1> Instead of `@Document(collation=...)`. +<2> Instead of `@Query(collation=...)`. +<3> Favors `@Collation` over meta usage. ==== include::./mongo-json-schema.adoc[leveloffset=+1]