Skip to content

Commit 0d752fd

Browse files
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
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+
}
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

+5
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

+5
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

+36-33
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

+5
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

+3
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

+3
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

+4
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
}

0 commit comments

Comments
 (0)