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]