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