Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0d752fd

Browse files
committedAug 25, 2022
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
1 parent 8aabf2f commit 0d752fd

File tree

15 files changed

+317
-54
lines changed

15 files changed

+317
-54
lines changed
 
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.annotation;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Inherited;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* {@link Collation} allows to define the rules used for language-specific string comparison.
26+
*
27+
* @see <a href="https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
28+
* @author Christoph Strobl
29+
* @since 4.0
30+
*/
31+
@Inherited
32+
@Retention(RetentionPolicy.RUNTIME)
33+
@Target({ ElementType.TYPE, ElementType.METHOD })
34+
public @interface Collation {
35+
36+
/**
37+
* The actual collation definition in JSON format or a
38+
* {@link org.springframework.expression.spel.standard.SpelExpression template expression} resolving to either a JSON
39+
* String or a {@link org.bson.Document}. The keys of the JSON document are configuration options for the collation.
40+
*
41+
* @return an empty {@link String} by default.
42+
*/
43+
String value() default "";
44+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Core Spring Data MongoDB annotations not limited to a special use case (like Query,...).
3+
*/
4+
@org.springframework.lang.NonNullApi
5+
package org.springframework.data.mongodb.core.annotation;
6+

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424

25+
import org.springframework.core.annotation.AliasFor;
26+
import org.springframework.data.mongodb.core.annotation.Collation;
2527
import org.springframework.data.mongodb.core.mapping.Document;
28+
2629
/**
2730
* Mark a class to use compound indexes. <br />
2831
* <p>
@@ -49,6 +52,7 @@
4952
* @author Dave Perryman
5053
* @author Stefan Tirea
5154
*/
55+
@Collation
5256
@Target({ ElementType.TYPE })
5357
@Documented
5458
@Repeatable(CompoundIndexes.class)
@@ -181,5 +185,6 @@
181185
* "https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
182186
* @since 4.0
183187
*/
188+
@AliasFor(annotation = Collation.class, attribute = "value")
184189
String collation() default "";
185190
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
import java.lang.annotation.RetentionPolicy;
2121
import java.lang.annotation.Target;
2222

23+
import org.springframework.core.annotation.AliasFor;
24+
import org.springframework.data.mongodb.core.annotation.Collation;
2325
import org.springframework.data.mongodb.core.mapping.Document;
26+
2427
/**
2528
* Mark a field to be indexed using MongoDB's indexing feature.
2629
*
@@ -34,6 +37,7 @@
3437
* @author Mark Paluch
3538
* @author Stefan Tirea
3639
*/
40+
@Collation
3741
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD })
3842
@Retention(RetentionPolicy.RUNTIME)
3943
public @interface Indexed {
@@ -188,5 +192,6 @@
188192
* @see <a href="https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
189193
* @since 4.0
190194
*/
195+
@AliasFor(annotation = Collation.class, attribute = "value")
191196
String collation() default "";
192197
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.core.index;
1717

18+
import java.lang.annotation.Annotation;
1819
import java.time.Duration;
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
@@ -23,13 +24,15 @@
2324
import java.util.HashSet;
2425
import java.util.Iterator;
2526
import java.util.List;
27+
import java.util.Map;
2628
import java.util.Set;
2729
import java.util.concurrent.TimeUnit;
30+
import java.util.function.Supplier;
2831
import java.util.stream.Collectors;
2932

3033
import org.apache.commons.logging.Log;
3134
import org.apache.commons.logging.LogFactory;
32-
35+
import org.springframework.core.annotation.MergedAnnotation;
3336
import org.springframework.dao.InvalidDataAccessApiUsageException;
3437
import org.springframework.data.domain.Sort;
3538
import org.springframework.data.mapping.Association;
@@ -50,12 +53,10 @@
5053
import org.springframework.data.mongodb.core.query.Collation;
5154
import org.springframework.data.mongodb.util.BsonUtils;
5255
import org.springframework.data.mongodb.util.DotPath;
56+
import org.springframework.data.mongodb.util.spel.ExpressionUtils;
5357
import org.springframework.data.spel.EvaluationContextProvider;
5458
import org.springframework.data.util.TypeInformation;
5559
import org.springframework.expression.EvaluationContext;
56-
import org.springframework.expression.Expression;
57-
import org.springframework.expression.ParserContext;
58-
import org.springframework.expression.common.LiteralExpression;
5960
import org.springframework.expression.spel.standard.SpelExpressionParser;
6061
import org.springframework.lang.Nullable;
6162
import org.springframework.util.Assert;
@@ -454,10 +455,7 @@ protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, St
454455
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
455456
}
456457

457-
if (StringUtils.hasText(index.collation())) {
458-
indexDefinition.collation(evaluateCollation(index.collation(), entity));
459-
}
460-
458+
indexDefinition.collation(resolveCollation(index, entity));
461459
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
462460
}
463461

@@ -478,12 +476,7 @@ protected IndexDefinitionHolder createWildcardIndexDefinition(String dotPath, St
478476
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
479477
}
480478

481-
if (StringUtils.hasText(index.collation())) {
482-
indexDefinition.collation(evaluateCollation(index.collation(), entity));
483-
} else if (entity != null && entity.hasCollation()) {
484-
indexDefinition.collation(entity.getCollation());
485-
}
486-
479+
indexDefinition.collation(resolveCollation(index, entity));
487480
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
488481
}
489482

@@ -498,7 +491,7 @@ private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dot
498491
return new org.bson.Document(dotPath, 1);
499492
}
500493

501-
Object keyDefToUse = evaluate(keyDefinitionString, getEvaluationContextForProperty(entity));
494+
Object keyDefToUse = ExpressionUtils.evaluate(keyDefinitionString, () -> getEvaluationContextForProperty(entity));
502495

503496
org.bson.Document dbo = (keyDefToUse instanceof org.bson.Document) ? (org.bson.Document) keyDefToUse
504497
: org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse));
@@ -567,7 +560,7 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col
567560
}
568561

569562
Duration timeout = computeIndexTimeout(index.expireAfter(),
570-
getEvaluationContextForProperty(persistentProperty.getOwner()));
563+
() -> getEvaluationContextForProperty(persistentProperty.getOwner()));
571564
if (!timeout.isZero() && !timeout.isNegative()) {
572565
indexDefinition.expire(timeout);
573566
}
@@ -577,16 +570,13 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col
577570
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), persistentProperty.getOwner()));
578571
}
579572

580-
if (StringUtils.hasText(index.collation())) {
581-
indexDefinition.collation(evaluateCollation(index.collation(), persistentProperty.getOwner()));
582-
}
583-
573+
indexDefinition.collation(resolveCollation(index, persistentProperty.getOwner()));
584574
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
585575
}
586576

587577
private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity<?, ?> entity) {
588578

589-
Object result = evaluate(filterExpression, getEvaluationContextForProperty(entity));
579+
Object result = ExpressionUtils.evaluate(filterExpression, () -> getEvaluationContextForProperty(entity));
590580

591581
if (result instanceof org.bson.Document) {
592582
return PartialIndexFilter.of((org.bson.Document) result);
@@ -597,7 +587,7 @@ private PartialIndexFilter evaluatePartialFilter(String filterExpression, Persis
597587

598588
private org.bson.Document evaluateWildcardProjection(String projectionExpression, PersistentEntity<?, ?> entity) {
599589

600-
Object result = evaluate(projectionExpression, getEvaluationContextForProperty(entity));
590+
Object result = ExpressionUtils.evaluate(projectionExpression, () -> getEvaluationContextForProperty(entity));
601591

602592
if (result instanceof org.bson.Document) {
603593
return (org.bson.Document) result;
@@ -608,7 +598,7 @@ private org.bson.Document evaluateWildcardProjection(String projectionExpression
608598

609599
private Collation evaluateCollation(String collationExpression, PersistentEntity<?, ?> entity) {
610600

611-
Object result = evaluate(collationExpression, getEvaluationContextForProperty(entity));
601+
Object result = ExpressionUtils.evaluate(collationExpression, () -> getEvaluationContextForProperty(entity));
612602
if (result instanceof org.bson.Document) {
613603
return Collation.from((org.bson.Document) result);
614604
}
@@ -618,6 +608,9 @@ private Collation evaluateCollation(String collationExpression, PersistentEntity
618608
if (result instanceof String) {
619609
return Collation.parse(result.toString());
620610
}
611+
if (result instanceof Map) {
612+
return Collation.from(new org.bson.Document((Map<String, ?>) result));
613+
}
621614
throw new IllegalStateException("Cannot parse collation " + result);
622615

623616
}
@@ -726,7 +719,7 @@ private String pathAwareIndexName(String indexName, String dotPath, @Nullable Pe
726719
String nameToUse = "";
727720
if (StringUtils.hasText(indexName)) {
728721

729-
Object result = evaluate(indexName, getEvaluationContextForProperty(entity));
722+
Object result = ExpressionUtils.evaluate(indexName, () -> getEvaluationContextForProperty(entity));
730723

731724
if (result != null) {
732725
nameToUse = ObjectUtils.nullSafeToString(result);
@@ -787,9 +780,9 @@ private void resolveAndAddIndexesForAssociation(Association<MongoPersistentPrope
787780
* @since 2.2
788781
* @throws IllegalArgumentException for invalid duration values.
789782
*/
790-
private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) {
783+
private static Duration computeIndexTimeout(String timeoutValue, Supplier<EvaluationContext> evaluationContext) {
791784

792-
Object evaluatedTimeout = evaluate(timeoutValue, evaluationContext);
785+
Object evaluatedTimeout = ExpressionUtils.evaluate(timeoutValue, evaluationContext);
793786

794787
if (evaluatedTimeout == null) {
795788
return Duration.ZERO;
@@ -808,15 +801,25 @@ private static Duration computeIndexTimeout(String timeoutValue, EvaluationConte
808801
return DurationStyle.detectAndParse(val);
809802
}
810803

804+
/**
805+
* Resolve the "collation" attribute from a given {@link Annotation} if present.
806+
*
807+
* @param annotation
808+
* @param entity
809+
* @return the collation present on either the annotation or the entity as a fallback. Might be {@literal null}.
810+
* @since 4.0
811+
*/
811812
@Nullable
812-
private static Object evaluate(String value, EvaluationContext evaluationContext) {
813+
private Collation resolveCollation(Annotation annotation, @Nullable PersistentEntity<?, ?> entity) {
814+
return MergedAnnotation.from(annotation).getValue("collation", String.class).filter(StringUtils::hasText)
815+
.map(it -> evaluateCollation(it, entity)).orElseGet(() -> {
813816

814-
Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
815-
if (expression instanceof LiteralExpression) {
816-
return value;
817-
}
818-
819-
return expression.getValue(evaluationContext, Object.class);
817+
if (entity instanceof MongoPersistentEntity<?> mongoPersistentEntity
818+
&& mongoPersistentEntity.hasCollation()) {
819+
return mongoPersistentEntity.getCollation();
820+
}
821+
return null;
822+
});
820823
}
821824

822825
private static boolean isMapWithoutWildcardIndex(MongoPersistentProperty property) {

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323

24+
import org.springframework.core.annotation.AliasFor;
25+
import org.springframework.data.mongodb.core.annotation.Collation;
26+
2427
/**
2528
* Annotation for an entity or property that should be used as key for a
2629
* <a href="https://docs.mongodb.com/manual/core/index-wildcard/">Wildcard Index</a>. <br />
@@ -79,6 +82,7 @@
7982
* @author Christoph Strobl
8083
* @since 3.3
8184
*/
85+
@Collation
8286
@Documented
8387
@Target({ ElementType.TYPE, ElementType.FIELD })
8488
@Retention(RetentionPolicy.RUNTIME)
@@ -126,5 +130,6 @@
126130
*
127131
* @return an empty {@link String} by default.
128132
*/
133+
@AliasFor(annotation = Collation.class, attribute = "value")
129134
String collation() default "";
130135
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.springframework.core.annotation.AliasFor;
2525
import org.springframework.data.annotation.Persistent;
26+
import org.springframework.data.mongodb.core.annotation.Collation;
2627

2728
/**
2829
* Identifies a domain object to be persisted to MongoDB.
@@ -32,6 +33,7 @@
3233
* @author Christoph Strobl
3334
*/
3435
@Persistent
36+
@Collation
3537
@Inherited
3638
@Retention(RetentionPolicy.RUNTIME)
3739
@Target({ ElementType.TYPE })
@@ -71,6 +73,7 @@
7173
* @return an empty {@link String} by default.
7274
* @since 2.2
7375
*/
76+
@AliasFor(annotation = Collation.class, attribute = "value")
7477
String collation() default "";
7578

7679
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.springframework.core.annotation.AliasFor;
2525
import org.springframework.data.annotation.QueryAnnotation;
26+
import org.springframework.data.mongodb.core.annotation.Collation;
2627

2728
/**
2829
* The {@link Aggregation} annotation can be used to annotate a {@link org.springframework.data.repository.Repository}
@@ -38,6 +39,7 @@
3839
* @author Christoph Strobl
3940
* @since 2.2
4041
*/
42+
@Collation
4143
@Retention(RetentionPolicy.RUNTIME)
4244
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
4345
@Documented
@@ -123,5 +125,6 @@
123125
*
124126
* @return an empty {@link String} by default.
125127
*/
128+
@AliasFor(annotation = Collation.class, attribute = "value")
126129
String collation() default "";
127130
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323

24+
import org.springframework.core.annotation.AliasFor;
2425
import org.springframework.data.annotation.QueryAnnotation;
26+
import org.springframework.data.mongodb.core.annotation.Collation;
2527

2628
/**
2729
* Annotation to declare finder queries directly on repository methods. Both attributes allow using a placeholder
@@ -32,6 +34,7 @@
3234
* @author Christoph Strobl
3335
* @author Mark Paluch
3436
*/
37+
@Collation
3538
@Retention(RetentionPolicy.RUNTIME)
3639
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
3740
@Documented
@@ -124,5 +127,6 @@
124127
* @return an empty {@link String} by default.
125128
* @since 2.2
126129
*/
130+
@AliasFor(annotation = Collation.class, attribute = "value")
127131
String collation() default "";
128132
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.data.geo.GeoResult;
2929
import org.springframework.data.geo.GeoResults;
3030
import org.springframework.data.mapping.context.MappingContext;
31+
import org.springframework.data.mongodb.core.annotation.Collation;
3132
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
3233
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
3334
import org.springframework.data.mongodb.core.query.UpdateDefinition;
@@ -321,14 +322,7 @@ public String getAnnotatedSort() {
321322
* @since 2.2
322323
*/
323324
public boolean hasAnnotatedCollation() {
324-
325-
Optional<String> optionalCollation = lookupQueryAnnotation().map(Query::collation);
326-
327-
if (!optionalCollation.isPresent()) {
328-
optionalCollation = lookupAggregationAnnotation().map(Aggregation::collation);
329-
}
330-
331-
return optionalCollation.filter(StringUtils::hasText).isPresent();
325+
return doFindAnnotation(Collation.class).map(Collation::value).filter(StringUtils::hasText).isPresent();
332326
}
333327

334328
/**
@@ -341,10 +335,9 @@ public boolean hasAnnotatedCollation() {
341335
*/
342336
public String getAnnotatedCollation() {
343337

344-
return lookupQueryAnnotation().map(Query::collation)
345-
.orElseGet(() -> lookupAggregationAnnotation().map(Aggregation::collation) //
338+
return doFindAnnotation(Collation.class).map(Collation::value) //
346339
.orElseThrow(() -> new IllegalStateException(
347-
"Expected to find @Query annotation but did not; Make sure to check hasAnnotatedCollation() before.")));
340+
"Expected to find @Collation annotation but did not; Make sure to check hasAnnotatedCollation() before."));
348341
}
349342

350343
/**
@@ -447,7 +440,7 @@ public void verify() {
447440
private boolean isNumericOrVoidReturnValue() {
448441

449442
Class<?> resultType = getReturnedObjectType();
450-
if(ReactiveWrappers.usesReactiveType(resultType)) {
443+
if (ReactiveWrappers.usesReactiveType(resultType)) {
451444
resultType = getReturnType().getComponentType().getType();
452445
}
453446

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package org.springframework.data.mongodb.util.spel;
1717

18+
import java.util.function.Supplier;
19+
20+
import org.springframework.expression.EvaluationContext;
1821
import org.springframework.expression.Expression;
1922
import org.springframework.expression.ParserContext;
2023
import org.springframework.expression.common.LiteralExpression;
@@ -49,4 +52,15 @@ public static Expression detectExpression(@Nullable String potentialExpression)
4952
Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION);
5053
return expression instanceof LiteralExpression ? null : expression;
5154
}
55+
56+
@Nullable
57+
public static Object evaluate(String value, Supplier<EvaluationContext> evaluationContext) {
58+
59+
Expression expression = detectExpression(value);
60+
if (expression == null) {
61+
return value;
62+
}
63+
64+
return expression.getValue(evaluationContext.get(), Object.class);
65+
}
5266
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,32 @@ public void compoundIndexWithCollation() {
713713
assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1));
714714
}
715715

716+
@Test // GH-3002
717+
public void compoundIndexWithCollationFromDocumentAnnotation() {
718+
719+
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
720+
WithCompoundCollationFromDocument.class);
721+
722+
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
723+
assertThat(indexDefinition.getIndexOptions())
724+
.isEqualTo(new org.bson.Document().append("name", "compound_index_with_collation").append("collation",
725+
new org.bson.Document().append("locale", "en_US").append("strength", 2)));
726+
assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1));
727+
}
728+
729+
@Test // GH-3002
730+
public void compoundIndexWithEvaluatedCollationFromAnnotation() {
731+
732+
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
733+
WithEvaluatedCollationFromCompoundIndex.class);
734+
735+
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
736+
assertThat(indexDefinition.getIndexOptions())
737+
.isEqualTo(new org.bson.Document().append("name", "compound_index_with_collation").append("collation",
738+
new org.bson.Document().append("locale", "de_AT")));
739+
assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1));
740+
}
741+
716742
@Document("CompoundIndexOnLevelOne")
717743
class CompoundIndexOnLevelOne {
718744

@@ -793,6 +819,14 @@ class SingleCompoundIndexWithPartialFilter {}
793819
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}",
794820
collation = "{'locale': 'en_US', 'strength': 2}")
795821
class CompoundIndexWithCollation {}
822+
823+
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
824+
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}")
825+
class WithCompoundCollationFromDocument {}
826+
827+
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
828+
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}", collation = "#{{ 'locale' : 'de' + '_' + 'AT' }}")
829+
class WithEvaluatedCollationFromCompoundIndex {}
796830
}
797831

798832
public static class TextIndexedResolutionTests {
@@ -1423,14 +1457,37 @@ public void shouldSkipMapStructuresUnlessAnnotatedWithWildcardIndex() {
14231457
public void indexedWithCollation() {
14241458

14251459
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
1426-
IndexedWithCollation.class);
1460+
WithCollationFromIndexedAnnotation.class);
1461+
1462+
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
1463+
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
1464+
.append("unique", true)
1465+
.append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2)));
1466+
}
1467+
1468+
@Test // GH-3002
1469+
public void indexedWithCollationFromDocumentAnnotation() {
1470+
1471+
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
1472+
WithCollationFromDocumentAnnotation.class);
14271473

14281474
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
14291475
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
14301476
.append("unique", true)
14311477
.append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2)));
14321478
}
14331479

1480+
@Test // GH-3002
1481+
public void indexedWithEvaluatedCollation() {
1482+
1483+
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
1484+
WithEvaluatedCollationFromIndexedAnnotation.class);
1485+
1486+
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
1487+
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
1488+
.append("collation", new org.bson.Document().append("locale", "de_AT")));
1489+
}
1490+
14341491
@Document
14351492
class MixedIndexRoot {
14361493

@@ -1749,11 +1806,26 @@ class WithComposedHashedIndexAndIndex {
17491806
}
17501807

17511808
@Document
1752-
class IndexedWithCollation {
1809+
class WithCollationFromIndexedAnnotation {
1810+
17531811
@Indexed(collation = "{'locale': 'en_US', 'strength': 2}", unique = true) //
17541812
private String value;
17551813
}
17561814

1815+
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
1816+
class WithCollationFromDocumentAnnotation {
1817+
1818+
@Indexed(unique = true) //
1819+
private String value;
1820+
}
1821+
1822+
@Document(collation = "en_US")
1823+
class WithEvaluatedCollationFromIndexedAnnotation {
1824+
1825+
@Indexed(collation = "#{{'locale' : 'de' + '_' + 'AT'}}") //
1826+
private String value;
1827+
}
1828+
17571829
@HashIndexed
17581830
@Indexed
17591831
@Retention(RetentionPolicy.RUNTIME)

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.data.geo.Point;
3232
import org.springframework.data.mongodb.core.User;
3333
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
34+
import org.springframework.data.mongodb.core.annotation.Collation;
3435
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
3536
import org.springframework.data.mongodb.core.query.Update;
3637
import org.springframework.data.mongodb.core.query.UpdateDefinition;
@@ -39,6 +40,7 @@
3940
import org.springframework.data.mongodb.repository.Contact;
4041
import org.springframework.data.mongodb.repository.Meta;
4142
import org.springframework.data.mongodb.repository.Person;
43+
import org.springframework.data.mongodb.repository.Query;
4244
import org.springframework.data.projection.ProjectionFactory;
4345
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
4446
import org.springframework.data.repository.Repository;
@@ -278,6 +280,33 @@ void queryCreationForUpdateMethodFailsOnInvalidReturnType() throws Exception {
278280
.withMessageContaining("findAndIncrementVisitsByFirstname");
279281
}
280282

283+
@Test // GH-3002
284+
void readsCollationFromAtCollationAnnotation() throws Exception {
285+
286+
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtCollationByFirstname", String.class);
287+
288+
assertThat(method.hasAnnotatedCollation()).isTrue();
289+
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
290+
}
291+
292+
@Test // GH-3002
293+
void readsCollationFromAtQueryAnnotation() throws Exception {
294+
295+
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtQueryByFirstname", String.class);
296+
297+
assertThat(method.hasAnnotatedCollation()).isTrue();
298+
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
299+
}
300+
301+
@Test // GH-3002
302+
void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception {
303+
304+
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class);
305+
306+
assertThat(method.hasAnnotatedCollation()).isTrue();
307+
assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT");
308+
}
309+
281310
private MongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception {
282311

283312
Method method = repository.getMethod(name, parameters);
@@ -338,6 +367,16 @@ interface PersonRepository extends Repository<User, Long> {
338367
void findAndUpdateBy(String firstname, UpdateDefinition update);
339368

340369
void findAndUpdateBy(String firstname, AggregationUpdate update);
370+
371+
@Collation("en_US")
372+
List<User> findWithCollationFromAtCollationByFirstname(String firstname);
373+
374+
@Query(collation = "en_US")
375+
List<User> findWithCollationFromAtQueryByFirstname(String firstname);
376+
377+
@Collation("de_AT")
378+
@Query(collation = "en_US")
379+
List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname);
341380
}
342381

343382
interface SampleRepository extends Repository<Contact, Long> {

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Test;
22+
import org.springframework.data.mongodb.core.annotation.Collation;
23+
import org.springframework.data.mongodb.repository.Query;
2024
import reactor.core.publisher.Flux;
2125
import reactor.core.publisher.Mono;
2226

2327
import java.lang.reflect.Method;
2428
import java.util.List;
2529

2630
import org.assertj.core.api.Assertions;
27-
import org.junit.Before;
28-
import org.junit.Test;
2931

3032
import org.springframework.dao.InvalidDataAccessApiUsageException;
3133
import org.springframework.data.domain.Page;
@@ -56,7 +58,7 @@ public class ReactiveMongoQueryMethodUnitTests {
5658

5759
MongoMappingContext context;
5860

59-
@Before
61+
@BeforeEach
6062
public void setUp() {
6163
context = new MongoMappingContext();
6264
}
@@ -102,13 +104,13 @@ public void discoversUserAsDomainTypeForGeoPagingQueryMethod() throws Exception
102104
.isTrue();
103105
}
104106

105-
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1444
107+
@Test // DATAMONGO-1444
106108
public void rejectsNullMappingContext() throws Exception {
107109

108110
Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Point.class);
109111

110-
new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class),
111-
new SpelAwareProxyProjectionFactory(), null);
112+
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class),
113+
new SpelAwareProxyProjectionFactory(), null));
112114
}
113115

114116
@Test // DATAMONGO-1444
@@ -197,6 +199,33 @@ public void queryCreationForUpdateMethodFailsOnInvalidReturnType() throws Except
197199
.withMessageContaining("findAndIncrementVisitsByFirstname");
198200
}
199201

202+
@Test // GH-3002
203+
void readsCollationFromAtCollationAnnotation() throws Exception {
204+
205+
ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithCollationFromAtCollationByFirstname", String.class);
206+
207+
assertThat(method.hasAnnotatedCollation()).isTrue();
208+
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
209+
}
210+
211+
@Test // GH-3002
212+
void readsCollationFromAtQueryAnnotation() throws Exception {
213+
214+
ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithCollationFromAtQueryByFirstname", String.class);
215+
216+
assertThat(method.hasAnnotatedCollation()).isTrue();
217+
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
218+
}
219+
220+
@Test // GH-3002
221+
void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception {
222+
223+
ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class);
224+
225+
assertThat(method.hasAnnotatedCollation()).isTrue();
226+
assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT");
227+
}
228+
200229
private ReactiveMongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters)
201230
throws Exception {
202231

@@ -238,6 +267,16 @@ interface PersonRepository extends Repository<User, Long> {
238267
@Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }",
239268
collation = "de_AT")
240269
Flux<User> findByAggregationWithCollation();
270+
271+
@Collation("en_US")
272+
List<User> findWithCollationFromAtCollationByFirstname(String firstname);
273+
274+
@Query(collation = "en_US")
275+
List<User> findWithCollationFromAtQueryByFirstname(String firstname);
276+
277+
@Collation("de_AT")
278+
@Query(collation = "en_US")
279+
List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname);
241280
}
242281

243282
interface SampleRepository extends Repository<Contact, Long> {

‎src/main/asciidoc/reference/mongodb.adoc

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2011,7 +2011,35 @@ and `Document` (eg. new Document("locale", "en_US"))
20112011
NOTE: In case you enabled the automatic index creation for repository finder methods a potential static collation definition,
20122012
as shown in (1) and (2), will be included when creating the index.
20132013
2014-
TIP: The most specifc `Collation` outroules potentially defined others. Which means Method argument over query method annotation over doamin type annotation.
2014+
TIP: The most specifc `Collation` outrules potentially defined others. Which means Method argument over query method annotation over domain type annotation.
2015+
====
2016+
2017+
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.
2018+
The same rules and locations apply, plus, direct usage of `@Collation` supersedes any collation values defined on `@Query` and other annotations.
2019+
Which means, if a collation is declared via `@Query` and additionally via `@Collation`, then the one from `@Collation` is picked.
2020+
2021+
.Using `@Collation`
2022+
====
2023+
[source,java]
2024+
----
2025+
@Collation("en_US") <1>
2026+
class Game {
2027+
// ...
2028+
}
2029+
2030+
interface GameRepository extends Repository<Game, String> {
2031+
2032+
@Collation("en_GB") <2>
2033+
List<Game> findByTitle(String title);
2034+
2035+
@Collation("de_AT") <3>
2036+
@Query(collation="en_GB")
2037+
List<Game> findByDescriptionContaining(String keyword);
2038+
}
2039+
----
2040+
<1> Instead of `@Document(collation=...)`.
2041+
<2> Instead of `@Query(collation=...)`.
2042+
<3> Favors `@Collation` over meta usage.
20152043
====
20162044

20172045
include::./mongo-json-schema.adoc[leveloffset=+1]

0 commit comments

Comments
 (0)
Please sign in to comment.