Browse Source

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
pull/4154/head
Christoph Strobl 3 years ago
parent
commit
0d752fd6e6
No known key found for this signature in database
GPG Key ID: 8CC1AB53391458C8
  1. 44
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/Collation.java
  2. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java
  3. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java
  4. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java
  5. 67
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java
  6. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java
  7. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java
  8. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java
  9. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
  10. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
  11. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java
  12. 76
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
  13. 39
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java
  14. 51
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java
  15. 30
      src/main/asciidoc/reference/mongodb.adoc

44
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 <a href="https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
* @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 "";
}

6
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;

5
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.RetentionPolicy;
import java.lang.annotation.Target; 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; import org.springframework.data.mongodb.core.mapping.Document;
/** /**
* Mark a class to use compound indexes. <br /> * Mark a class to use compound indexes. <br />
* <p> * <p>
@ -49,6 +52,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @author Dave Perryman * @author Dave Perryman
* @author Stefan Tirea * @author Stefan Tirea
*/ */
@Collation
@Target({ ElementType.TYPE }) @Target({ ElementType.TYPE })
@Documented @Documented
@Repeatable(CompoundIndexes.class) @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/</a> * "https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
* @since 4.0 * @since 4.0
*/ */
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default ""; String collation() default "";
} }

5
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.RetentionPolicy;
import java.lang.annotation.Target; 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; import org.springframework.data.mongodb.core.mapping.Document;
/** /**
* Mark a field to be indexed using MongoDB's indexing feature. * 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 Mark Paluch
* @author Stefan Tirea * @author Stefan Tirea
*/ */
@Collation
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD }) @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface Indexed { public @interface Indexed {
@ -188,5 +192,6 @@ public @interface Indexed {
* @see <a href="https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a> * @see <a href="https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
* @since 4.0 * @since 4.0
*/ */
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default ""; String collation() default "";
} }

67
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; package org.springframework.data.mongodb.core.index;
import java.lang.annotation.Annotation;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -23,13 +24,15 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.Association; 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.core.query.Collation;
import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.mongodb.util.DotPath; 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.spel.EvaluationContextProvider;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext; 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.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -454,10 +455,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity)); indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
} }
if (StringUtils.hasText(index.collation())) { indexDefinition.collation(resolveCollation(index, entity));
indexDefinition.collation(evaluateCollation(index.collation(), entity));
}
return new IndexDefinitionHolder(dotPath, indexDefinition, collection); return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
} }
@ -478,12 +476,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity)); indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
} }
if (StringUtils.hasText(index.collation())) { indexDefinition.collation(resolveCollation(index, entity));
indexDefinition.collation(evaluateCollation(index.collation(), entity));
} else if (entity != null && entity.hasCollation()) {
indexDefinition.collation(entity.getCollation());
}
return new IndexDefinitionHolder(dotPath, indexDefinition, collection); return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
} }
@ -498,7 +491,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return new org.bson.Document(dotPath, 1); 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 dbo = (keyDefToUse instanceof org.bson.Document) ? (org.bson.Document) keyDefToUse
: org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse)); : org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse));
@ -567,7 +560,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
} }
Duration timeout = computeIndexTimeout(index.expireAfter(), Duration timeout = computeIndexTimeout(index.expireAfter(),
getEvaluationContextForProperty(persistentProperty.getOwner())); () -> getEvaluationContextForProperty(persistentProperty.getOwner()));
if (!timeout.isZero() && !timeout.isNegative()) { if (!timeout.isZero() && !timeout.isNegative()) {
indexDefinition.expire(timeout); indexDefinition.expire(timeout);
} }
@ -577,16 +570,13 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), persistentProperty.getOwner())); indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), persistentProperty.getOwner()));
} }
if (StringUtils.hasText(index.collation())) { indexDefinition.collation(resolveCollation(index, persistentProperty.getOwner()));
indexDefinition.collation(evaluateCollation(index.collation(), persistentProperty.getOwner()));
}
return new IndexDefinitionHolder(dotPath, indexDefinition, collection); return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
} }
private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity<?, ?> entity) { 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) { if (result instanceof org.bson.Document) {
return PartialIndexFilter.of((org.bson.Document) result); 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) { 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) { if (result instanceof org.bson.Document) {
return (org.bson.Document) result; return (org.bson.Document) result;
@ -608,7 +598,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
private Collation evaluateCollation(String collationExpression, PersistentEntity<?, ?> entity) { 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) { if (result instanceof org.bson.Document) {
return Collation.from((org.bson.Document) result); return Collation.from((org.bson.Document) result);
} }
@ -618,6 +608,9 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
if (result instanceof String) { if (result instanceof String) {
return Collation.parse(result.toString()); return Collation.parse(result.toString());
} }
if (result instanceof Map) {
return Collation.from(new org.bson.Document((Map<String, ?>) result));
}
throw new IllegalStateException("Cannot parse collation " + result); throw new IllegalStateException("Cannot parse collation " + result);
} }
@ -726,7 +719,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
String nameToUse = ""; String nameToUse = "";
if (StringUtils.hasText(indexName)) { if (StringUtils.hasText(indexName)) {
Object result = evaluate(indexName, getEvaluationContextForProperty(entity)); Object result = ExpressionUtils.evaluate(indexName, () -> getEvaluationContextForProperty(entity));
if (result != null) { if (result != null) {
nameToUse = ObjectUtils.nullSafeToString(result); nameToUse = ObjectUtils.nullSafeToString(result);
@ -787,9 +780,9 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
* @since 2.2 * @since 2.2
* @throws IllegalArgumentException for invalid duration values. * @throws IllegalArgumentException for invalid duration values.
*/ */
private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) { private static Duration computeIndexTimeout(String timeoutValue, Supplier<EvaluationContext> evaluationContext) {
Object evaluatedTimeout = evaluate(timeoutValue, evaluationContext); Object evaluatedTimeout = ExpressionUtils.evaluate(timeoutValue, evaluationContext);
if (evaluatedTimeout == null) { if (evaluatedTimeout == null) {
return Duration.ZERO; return Duration.ZERO;
@ -808,15 +801,25 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return DurationStyle.detectAndParse(val); 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 @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 (entity instanceof MongoPersistentEntity<?> mongoPersistentEntity
if (expression instanceof LiteralExpression) { && mongoPersistentEntity.hasCollation()) {
return value; return mongoPersistentEntity.getCollation();
} }
return null;
return expression.getValue(evaluationContext, Object.class); });
} }
private static boolean isMapWithoutWildcardIndex(MongoPersistentProperty property) { private static boolean isMapWithoutWildcardIndex(MongoPersistentProperty property) {

5
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.RetentionPolicy;
import java.lang.annotation.Target; 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 * Annotation for an entity or property that should be used as key for a
* <a href="https://docs.mongodb.com/manual/core/index-wildcard/">Wildcard Index</a>. <br /> * <a href="https://docs.mongodb.com/manual/core/index-wildcard/">Wildcard Index</a>. <br />
@ -79,6 +82,7 @@ import java.lang.annotation.Target;
* @author Christoph Strobl * @author Christoph Strobl
* @since 3.3 * @since 3.3
*/ */
@Collation
@Documented @Documented
@Target({ ElementType.TYPE, ElementType.FIELD }) @Target({ ElementType.TYPE, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ -126,5 +130,6 @@ public @interface WildcardIndexed {
* *
* @return an empty {@link String} by default. * @return an empty {@link String} by default.
*/ */
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default ""; String collation() default "";
} }

3
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.core.annotation.AliasFor;
import org.springframework.data.annotation.Persistent; import org.springframework.data.annotation.Persistent;
import org.springframework.data.mongodb.core.annotation.Collation;
/** /**
* Identifies a domain object to be persisted to MongoDB. * Identifies a domain object to be persisted to MongoDB.
@ -32,6 +33,7 @@ import org.springframework.data.annotation.Persistent;
* @author Christoph Strobl * @author Christoph Strobl
*/ */
@Persistent @Persistent
@Collation
@Inherited @Inherited
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE }) @Target({ ElementType.TYPE })
@ -71,6 +73,7 @@ public @interface Document {
* @return an empty {@link String} by default. * @return an empty {@link String} by default.
* @since 2.2 * @since 2.2
*/ */
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default ""; String collation() default "";
} }

3
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.core.annotation.AliasFor;
import org.springframework.data.annotation.QueryAnnotation; 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} * 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 * @author Christoph Strobl
* @since 2.2 * @since 2.2
*/ */
@Collation
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented @Documented
@ -123,5 +125,6 @@ public @interface Aggregation {
* *
* @return an empty {@link String} by default. * @return an empty {@link String} by default.
*/ */
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default ""; String collation() default "";
} }

4
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.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.QueryAnnotation; 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 * 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 Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
*/ */
@Collation
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented @Documented
@ -124,5 +127,6 @@ public @interface Query {
* @return an empty {@link String} by default. * @return an empty {@link String} by default.
* @since 2.2 * @since 2.2
*/ */
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default ""; String collation() default "";
} }

15
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.GeoResult;
import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.GeoResults;
import org.springframework.data.mapping.context.MappingContext; 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.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition;
@ -321,14 +322,7 @@ public class MongoQueryMethod extends QueryMethod {
* @since 2.2 * @since 2.2
*/ */
public boolean hasAnnotatedCollation() { public boolean hasAnnotatedCollation() {
return doFindAnnotation(Collation.class).map(Collation::value).filter(StringUtils::hasText).isPresent();
Optional<String> optionalCollation = lookupQueryAnnotation().map(Query::collation);
if (!optionalCollation.isPresent()) {
optionalCollation = lookupAggregationAnnotation().map(Aggregation::collation);
}
return optionalCollation.filter(StringUtils::hasText).isPresent();
} }
/** /**
@ -341,10 +335,9 @@ public class MongoQueryMethod extends QueryMethod {
*/ */
public String getAnnotatedCollation() { public String getAnnotatedCollation() {
return lookupQueryAnnotation().map(Query::collation) return doFindAnnotation(Collation.class).map(Collation::value) //
.orElseGet(() -> lookupAggregationAnnotation().map(Aggregation::collation) //
.orElseThrow(() -> new IllegalStateException( .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."));
} }
/** /**

14
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; 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.Expression;
import org.springframework.expression.ParserContext; import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.common.LiteralExpression;
@ -49,4 +52,15 @@ public final class ExpressionUtils {
Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION); Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION);
return expression instanceof LiteralExpression ? null : expression; return expression instanceof LiteralExpression ? null : expression;
} }
@Nullable
public static Object evaluate(String value, Supplier<EvaluationContext> evaluationContext) {
Expression expression = detectExpression(value);
if (expression == null) {
return value;
}
return expression.getValue(evaluationContext.get(), Object.class);
}
} }

76
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)); assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1));
} }
@Test // GH-3002
public void compoundIndexWithCollationFromDocumentAnnotation() {
List<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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") @Document("CompoundIndexOnLevelOne")
class CompoundIndexOnLevelOne { class CompoundIndexOnLevelOne {
@ -793,6 +819,14 @@ public class MongoPersistentEntityIndexResolverUnitTests {
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}", @CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}",
collation = "{'locale': 'en_US', 'strength': 2}") collation = "{'locale': 'en_US', 'strength': 2}")
class CompoundIndexWithCollation {} 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 { public static class TextIndexedResolutionTests {
@ -1423,7 +1457,19 @@ public class MongoPersistentEntityIndexResolverUnitTests {
public void indexedWithCollation() { public void indexedWithCollation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType( List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
IndexedWithCollation.class); WithCollationFromIndexedAnnotation.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 indexedWithCollationFromDocumentAnnotation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
WithCollationFromDocumentAnnotation.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value") assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
@ -1431,6 +1477,17 @@ public class MongoPersistentEntityIndexResolverUnitTests {
.append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2))); .append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2)));
} }
@Test // GH-3002
public void indexedWithEvaluatedCollation() {
List<IndexDefinitionHolder> 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 @Document
class MixedIndexRoot { class MixedIndexRoot {
@ -1749,11 +1806,26 @@ public class MongoPersistentEntityIndexResolverUnitTests {
} }
@Document @Document
class IndexedWithCollation { class WithCollationFromIndexedAnnotation {
@Indexed(collation = "{'locale': 'en_US', 'strength': 2}", unique = true) // @Indexed(collation = "{'locale': 'en_US', 'strength': 2}", unique = true) //
private String value; 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 @HashIndexed
@Indexed @Indexed
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)

39
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.geo.Point;
import org.springframework.data.mongodb.core.User; import org.springframework.data.mongodb.core.User;
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; 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.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.core.query.UpdateDefinition; 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.Contact;
import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.Person; 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.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
@ -278,6 +280,33 @@ public class MongoQueryMethodUnitTests {
.withMessageContaining("findAndIncrementVisitsByFirstname"); .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 { private MongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception {
Method method = repository.getMethod(name, parameters); Method method = repository.getMethod(name, parameters);
@ -338,6 +367,16 @@ public class MongoQueryMethodUnitTests {
void findAndUpdateBy(String firstname, UpdateDefinition update); void findAndUpdateBy(String firstname, UpdateDefinition update);
void findAndUpdateBy(String firstname, AggregationUpdate update); void findAndUpdateBy(String firstname, AggregationUpdate update);
@Collation("en_US")
List<User> findWithCollationFromAtCollationByFirstname(String firstname);
@Query(collation = "en_US")
List<User> findWithCollationFromAtQueryByFirstname(String firstname);
@Collation("de_AT")
@Query(collation = "en_US")
List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname);
} }
interface SampleRepository extends Repository<Contact, Long> { interface SampleRepository extends Repository<Contact, Long> {

51
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 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.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -24,8 +28,6 @@ import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@ -56,7 +58,7 @@ public class ReactiveMongoQueryMethodUnitTests {
MongoMappingContext context; MongoMappingContext context;
@Before @BeforeEach
public void setUp() { public void setUp() {
context = new MongoMappingContext(); context = new MongoMappingContext();
} }
@ -102,13 +104,13 @@ public class ReactiveMongoQueryMethodUnitTests {
.isTrue(); .isTrue();
} }
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1444 @Test // DATAMONGO-1444
public void rejectsNullMappingContext() throws Exception { public void rejectsNullMappingContext() throws Exception {
Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Point.class); Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Point.class);
new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class),
new SpelAwareProxyProjectionFactory(), null); new SpelAwareProxyProjectionFactory(), null));
} }
@Test // DATAMONGO-1444 @Test // DATAMONGO-1444
@ -197,6 +199,33 @@ public class ReactiveMongoQueryMethodUnitTests {
.withMessageContaining("findAndIncrementVisitsByFirstname"); .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) private ReactiveMongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters)
throws Exception { throws Exception {
@ -238,6 +267,16 @@ public class ReactiveMongoQueryMethodUnitTests {
@Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }", @Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }",
collation = "de_AT") collation = "de_AT")
Flux<User> findByAggregationWithCollation(); Flux<User> findByAggregationWithCollation();
@Collation("en_US")
List<User> findWithCollationFromAtCollationByFirstname(String firstname);
@Query(collation = "en_US")
List<User> findWithCollationFromAtQueryByFirstname(String firstname);
@Collation("de_AT")
@Query(collation = "en_US")
List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname);
} }
interface SampleRepository extends Repository<Contact, Long> { interface SampleRepository extends Repository<Contact, Long> {

30
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, 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. 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<Game, String> {
@Collation("en_GB") <2>
List<Game> findByTitle(String title);
@Collation("de_AT") <3>
@Query(collation="en_GB")
List<Game> 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] include::./mongo-json-schema.adoc[leveloffset=+1]

Loading…
Cancel
Save