{ 'locale' : '?0' } .
+ * @return never {@literal null}.
+ * @throws IllegalArgumentException if {@literal collation} is null.
+ * @since 2.2
+ */
+ public static Collation parse(String collation) {
+
+ Assert.notNull(collation, "Collation must not be null!");
+
+ return StringUtils.trimLeadingWhitespace(collation).startsWith("{") ? from(Document.parse(collation))
+ : of(collation);
+ }
+
/**
* Create new {@link Collation} from values in {@link Document}.
*
@@ -386,6 +403,26 @@ public String toString() {
return toDocument().toJson();
}
+ @Override
+ public boolean equals(Object o) {
+
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Collation that = (Collation) o;
+ return this.toDocument().equals(that.toDocument());
+ }
+
+ @Override
+ public int hashCode() {
+ return toDocument().hashCode();
+ }
+
private Collation copy() {
Collation collation = new Collation(locale);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
index 8f902aead3..0bc3e1618c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
@@ -95,4 +95,34 @@
* @since 2.1
*/
String sort() default "";
+
+ /**
+ * Defines the collation to apply when executing the query.
+ *
+ *
+ * // Fixed value
+ * @Query(collation = "en_US")
+ * List findAllByFixedCollation();
+ *
+ * // Fixed value as Document
+ * @Query(collation = "{ 'locale' : 'en_US' }")
+ * List findAllByFixedJsonCollation();
+ *
+ * // Dynamic value as String
+ * @Query(collation = "?0")
+ * List findAllByDynamicCollation(String collation);
+ *
+ * // Dynamic value as Document
+ * @Query(collation = "{ 'locale' : ?0 }")
+ * List findAllByDynamicJsonCollation(String collation);
+ *
+ * // SpEL expression
+ * @Query(collation = "?#{[0]}")
+ * List findAllByDynamicSpElCollation(String collation);
+ *
+ *
+ * @return an empty {@link String} by default.
+ * @since 2.2
+ */
+ String collation() default "";
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
index 4784b15f87..03dbc78ff9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.repository.query;
import org.bson.Document;
+
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
@@ -27,8 +28,10 @@
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagingGeoNearExecution;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution;
import org.springframework.data.repository.query.ParameterAccessor;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
/**
@@ -44,17 +47,24 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
private final MongoQueryMethod method;
private final MongoOperations operations;
private final ExecutableFind> executableFind;
+ private final SpelExpressionParser expressionParser;
+ private final QueryMethodEvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
*
* @param method must not be {@literal null}.
* @param operations must not be {@literal null}.
+ * @param expressionParser must not be {@literal null}.
+ * @param evaluationContextProvider must not be {@literal null}.
*/
- public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations) {
+ public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, SpelExpressionParser expressionParser,
+ QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(operations, "MongoOperations must not be null!");
Assert.notNull(method, "MongoQueryMethod must not be null!");
+ Assert.notNull(expressionParser, "SpelExpressionParser must not be null!");
+ Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!");
this.method = method;
this.operations = operations;
@@ -63,6 +73,8 @@ public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations) {
Class> type = metadata.getCollectionEntity().getType();
this.executableFind = operations.query(type);
+ this.expressionParser = expressionParser;
+ this.evaluationContextProvider = evaluationContextProvider;
}
/*
@@ -86,6 +98,7 @@ public Object execute(Object[] parameters) {
applyQueryMetaAttributesWhenPresent(query);
query = applyAnnotatedDefaultSortIfPresent(query);
+ query = applyAnnotatedCollationIfPresent(query, accessor);
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
Class> typeToRead = processor.getReturnedType().getTypeToRead();
@@ -154,6 +167,21 @@ Query applyAnnotatedDefaultSortIfPresent(Query query) {
return QueryUtils.decorateSort(query, Document.parse(method.getAnnotatedSort()));
}
+ /**
+ * If present apply a {@link org.springframework.data.mongodb.core.query.Collation} derived from the
+ * {@link org.springframework.data.repository.query.QueryMethod} the given {@link Query}.
+ *
+ * @param query must not be {@literal null}.
+ * @param accessor the {@link ParameterAccessor} used to obtain parameter placeholder replacement values.
+ * @return
+ * @since 2.2
+ */
+ Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor accessor) {
+
+ return QueryUtils.applyCollation(query, method.hasAnnotatedCollation() ? method.getAnnotatedCollation() : null,
+ accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider);
+ }
+
/**
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
index d5c341569b..343965d703 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
@@ -20,6 +20,7 @@
import org.bson.Document;
import org.reactivestreams.Publisher;
+
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.mongodb.core.MongoOperations;
@@ -33,8 +34,10 @@
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingConverter;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingExecution;
import org.springframework.data.repository.query.ParameterAccessor;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
/**
@@ -50,6 +53,8 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
private final ReactiveMongoOperations operations;
private final EntityInstantiators instantiators;
private final FindWithProjection> findOperationWithProjection;
+ private final SpelExpressionParser expressionParser;
+ private final QueryMethodEvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and
@@ -57,15 +62,22 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
*
* @param method must not be {@literal null}.
* @param operations must not be {@literal null}.
+ * @param expressionParser must not be {@literal null}.
+ * @param evaluationContextProvider must not be {@literal null}.
*/
- public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations) {
+ public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations,
+ SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(method, "MongoQueryMethod must not be null!");
Assert.notNull(operations, "ReactiveMongoOperations must not be null!");
+ Assert.notNull(expressionParser, "SpelExpressionParser must not be null!");
+ Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!");
this.method = method;
this.operations = operations;
this.instantiators = new EntityInstantiators();
+ this.expressionParser = expressionParser;
+ this.evaluationContextProvider = evaluationContextProvider;
MongoEntityMetadata> metadata = method.getEntityInformation();
Class> type = metadata.getCollectionEntity().getType();
@@ -105,12 +117,15 @@ private Object executeDeferred(Object[] parameters) {
private Object execute(MongoParameterAccessor parameterAccessor) {
- Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), parameterAccessor));
+ ConvertingParameterAccessor convertingParamterAccessor = new ConvertingParameterAccessor(operations.getConverter(),
+ parameterAccessor);
+ Query query = createQuery(convertingParamterAccessor);
applyQueryMetaAttributesWhenPresent(query);
query = applyAnnotatedDefaultSortIfPresent(query);
+ query = applyAnnotatedCollationIfPresent(query, convertingParamterAccessor);
- ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor);
+ ResultProcessor processor = method.getResultProcessor().withDynamicProjection(convertingParamterAccessor);
Class> typeToRead = processor.getReturnedType().getTypeToRead();
FindWithQuery> find = typeToRead == null //
@@ -119,7 +134,7 @@ private Object execute(MongoParameterAccessor parameterAccessor) {
String collection = method.getEntityInformation().getCollectionName();
- ReactiveMongoQueryExecution execution = getExecution(parameterAccessor,
+ ReactiveMongoQueryExecution execution = getExecution(convertingParamterAccessor,
new ResultProcessingConverter(processor, operations, instantiators), find);
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
@@ -195,6 +210,21 @@ Query applyAnnotatedDefaultSortIfPresent(Query query) {
return QueryUtils.decorateSort(query, Document.parse(method.getAnnotatedSort()));
}
+ /**
+ * If present apply a {@link org.springframework.data.mongodb.core.query.Collation} derived from the
+ * {@link org.springframework.data.repository.query.QueryMethod} the given {@link Query}.
+ *
+ * @param query must not be {@literal null}.
+ * @param accessor the {@link ParameterAccessor} used to obtain parameter placeholder replacement values.
+ * @return
+ * @since 2.2
+ */
+ Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor accessor) {
+
+ return QueryUtils.applyCollation(query, method.hasAnnotatedCollation() ? method.getAnnotatedCollation() : null,
+ accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider);
+ }
+
/**
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
index f44e799e14..6d98f30bce 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
@@ -29,6 +29,7 @@
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.util.TypeInformation;
@@ -135,6 +136,15 @@ public TextCriteria getFullText() {
return delegate.getFullText();
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation()
+ */
+ @Override
+ public Collation getCollation() {
+ return delegate.getCollation();
+ }
+
/**
* Converts the given value with the underlying {@link MongoWriter}.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java
index 51fa02479d..22242a4f90 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.repository.query;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.lang.Nullable;
@@ -61,4 +62,23 @@ default boolean isVersioned() {
default Object getVersion(T entity) {
return null;
}
+
+ /**
+ * Returns whether the entity defines a specific collation.
+ *
+ * @return {@literal true} if the entity defines a collation.
+ * @since 2.2
+ */
+ default boolean hasCollation() {
+ return getCollation() != null;
+ }
+
+ /**
+ * Return the collation for the entity or {@literal null} if {@link #hasCollation() not defined}.
+ *
+ * @return can be {@literal null}.
+ * @since 2.2
+ */
+ @Nullable
+ Collation getCollation();
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java
index f41a1fa120..43bdfcc22c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java
@@ -18,6 +18,7 @@
import org.springframework.data.domain.Range;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.lang.Nullable;
@@ -57,6 +58,15 @@ public interface MongoParameterAccessor extends ParameterAccessor {
@Nullable
TextCriteria getFullText();
+ /**
+ * Returns the {@link Collation} to be used for the query.
+ *
+ * @return {@literal null} if not set.
+ * @since 2.2
+ */
+ @Nullable
+ Collation getCollation();
+
/**
* Returns the raw parameter values of the underlying query method.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java
index 5db8b24fb9..e7800498a2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java
@@ -23,6 +23,7 @@
import org.springframework.data.domain.Range;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.repository.Near;
import org.springframework.data.mongodb.repository.query.MongoParameters.MongoParameter;
@@ -45,12 +46,13 @@ public class MongoParameters extends Parameters
private final int maxDistanceIndex;
private final @Nullable Integer fullTextIndex;
private final @Nullable Integer nearIndex;
+ private final @Nullable Integer collationIndex;
/**
* Creates a new {@link MongoParameters} instance from the given {@link Method} and {@link MongoQueryMethod}.
*
* @param method must not be {@literal null}.
- * @param queryMethod must not be {@literal null}.
+ * @param isGeoNearMethod indicate if this is a geo spatial query method
*/
public MongoParameters(Method method, boolean isGeoNearMethod) {
@@ -64,6 +66,7 @@ public MongoParameters(Method method, boolean isGeoNearMethod) {
this.rangeIndex = getTypeIndex(parameterTypeInfo, Range.class, Distance.class);
this.maxDistanceIndex = this.rangeIndex == -1 ? getTypeIndex(parameterTypeInfo, Distance.class, null) : -1;
+ this.collationIndex = getTypeIndex(parameterTypeInfo, Collation.class, null);
int index = findNearIndexInParameters(method);
if (index == -1 && isGeoNearMethod) {
@@ -74,7 +77,7 @@ public MongoParameters(Method method, boolean isGeoNearMethod) {
}
private MongoParameters(List parameters, int maxDistanceIndex, @Nullable Integer nearIndex,
- @Nullable Integer fullTextIndex, int rangeIndex) {
+ @Nullable Integer fullTextIndex, int rangeIndex, @Nullable Integer collationIndex) {
super(parameters);
@@ -82,6 +85,7 @@ private MongoParameters(List parameters, int maxDistanceIndex, @
this.fullTextIndex = fullTextIndex;
this.maxDistanceIndex = maxDistanceIndex;
this.rangeIndex = rangeIndex;
+ this.collationIndex = collationIndex;
}
private final int getNearIndex(List> parameterTypes) {
@@ -111,11 +115,11 @@ private int findNearIndexInParameters(Method method) {
MongoParameter param = createParameter(MethodParameter.forParameter(p));
if (param.isManuallyAnnotatedNearParameter()) {
- if(index == -1) {
+ if (index == -1) {
index = param.getIndex();
} else {
- throw new IllegalStateException(String.format("Found multiple @Near annotations ond method %s! Only one allowed!",
- method.toString()));
+ throw new IllegalStateException(
+ String.format("Found multiple @Near annotations ond method %s! Only one allowed!", method.toString()));
}
}
@@ -132,8 +136,6 @@ protected MongoParameter createParameter(MethodParameter parameter) {
return new MongoParameter(parameter);
}
-
-
public int getDistanceRangeIndex() {
return -1;
}
@@ -158,7 +160,7 @@ public int getNearIndex() {
}
/**
- * Returns ths inde of the parameter to be used as a textquery param
+ * Returns the index of the parameter to be used as a textquery param
*
* @return
* @since 1.6
@@ -183,13 +185,24 @@ public int getRangeIndex() {
return rangeIndex;
}
+ /**
+ * Returns the index of the {@link Collation} parameter or -1 if not present.
+ *
+ * @return -1 if not set.
+ * @since 2.2
+ */
+ public int getCollationParameterIndex() {
+ return collationIndex != null ? collationIndex.intValue() : -1;
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List)
*/
@Override
protected MongoParameters createFrom(List parameters) {
- return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex);
+ return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex,
+ this.collationIndex);
}
private int getTypeIndex(List> parameterTypes, Class> type, @Nullable Class> componentType) {
@@ -241,7 +254,7 @@ class MongoParameter extends Parameter {
@Override
public boolean isSpecialParameter() {
return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter()
- || TextCriteria.class.isAssignableFrom(getType());
+ || TextCriteria.class.isAssignableFrom(getType()) || Collation.class.isAssignableFrom(getType());
}
private boolean isNearParameter() {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java
index 3cf2e58bd2..2364f68628 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java
@@ -22,6 +22,7 @@
import org.springframework.data.domain.Range.Bound;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Term;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.ParametersParameterAccessor;
@@ -67,7 +68,8 @@ public Range getDistanceRange() {
}
int maxDistanceIndex = mongoParameters.getMaxDistanceIndex();
- Bound maxDistance = maxDistanceIndex == -1 ? Bound.unbounded() : Bound.inclusive((Distance) getValue(maxDistanceIndex));
+ Bound maxDistance = maxDistanceIndex == -1 ? Bound.unbounded()
+ : Bound.inclusive((Distance) getValue(maxDistanceIndex));
return Range.of(Bound.unbounded(), maxDistance);
}
@@ -134,6 +136,20 @@ protected TextCriteria potentiallyConvertFullText(Object fullText) {
ClassUtils.getShortName(fullText.getClass())));
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation()
+ */
+ @Override
+ public Collation getCollation() {
+
+ if (method.getParameters().getCollationParameterIndex() == -1) {
+ return null;
+ }
+
+ return getValue(method.getParameters().getCollationParameterIndex());
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues()
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
index 9bc7d00a1c..79caf54ec5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
@@ -322,6 +322,30 @@ public String getAnnotatedSort() {
"Expected to find @Query annotation but did not. Make sure to check hasAnnotatedSort() before."));
}
+ /**
+ * Check if the query method is decorated with an non empty {@link Query#collation()}.
+ *
+ * @return true if method annotated with {@link Query} having an non empty collation attribute.
+ * @since 2.2
+ */
+ public boolean hasAnnotatedCollation() {
+ return lookupQueryAnnotation().map(it -> !it.collation().isEmpty()).orElse(false);
+ }
+
+ /**
+ * Get the collation value extracted from the {@link Query} annotation.
+ *
+ * @return the {@link Query#collation()} value.
+ * @throws IllegalStateException if method not annotated with {@link Query}. Make sure to check
+ * {@link #hasAnnotatedQuery()} first.
+ * @since 2.2
+ */
+ public String getAnnotatedCollation() {
+
+ return lookupQueryAnnotation().map(Query::collation).orElseThrow(() -> new IllegalStateException(
+ "Expected to find @Query annotation but did not. Make sure to check hasAnnotatedCollation() before."));
+ }
+
@SuppressWarnings("unchecked")
private Optional doFindAnnotation(Class annotationType) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java
index 16d2aeb6d0..7a3f3e4b80 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java
@@ -17,6 +17,7 @@
import org.bson.Document;
import org.bson.json.JsonParseException;
+
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
@@ -26,10 +27,12 @@
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.StringUtils;
/**
@@ -52,10 +55,13 @@ public class PartTreeMongoQuery extends AbstractMongoQuery {
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
+ * @param expressionParser must not be {@literal null}.
+ * @param evaluationContextProvider must not be {@literal null}.
*/
- public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations) {
+ public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations,
+ SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
- super(method, mongoOperations);
+ super(method, mongoOperations, expressionParser, evaluationContextProvider);
this.processor = method.getResultProcessor();
this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
index c9cca1266b..d73f0b4152 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
@@ -15,21 +15,40 @@
*/
package org.springframework.data.mongodb.repository.query;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
import org.aopalliance.intercept.MethodInterceptor;
import org.bson.Document;
+
import org.springframework.aop.framework.ProxyFactory;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.util.json.ParameterBindingContext;
+import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.lang.Nullable;
+import org.springframework.util.NumberUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
/**
* Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Query} support
* offered by repositories.
*
* @author Christoph Strobl
+ * @author Mark Paluch
* @since 2.1
* @currentRead Assassin's Apprentice - Robin Hobb
*/
class QueryUtils {
+ private static final ParameterBindingDocumentCodec CODEC = new ParameterBindingDocumentCodec();
+
+ private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
+
/**
* Decorate {@link Query} and add a default sort expression to the given {@link Query}. Attributes of the given
* {@code sort} may be overwritten by the sort explicitly defined by the {@link Query} itself.
@@ -58,4 +77,65 @@ static Query decorateSort(Query query, Document defaultSort) {
return (Query) factory.getProxy();
}
+
+ /**
+ * Apply a collation extracted from the given {@literal collationExpression} to the given {@link Query}. Potentially
+ * replace parameter placeholders with values from the {@link ConvertingParameterAccessor accessor}.
+ *
+ * @param query must not be {@literal null}.
+ * @param collationExpression must not be {@literal null}.
+ * @param accessor must not be {@literal null}.
+ * @return the {@link Query} having proper {@link Collation}.
+ * @see Query#collation(Collation)
+ * @since 2.2
+ */
+ static Query applyCollation(Query query, @Nullable String collationExpression, ConvertingParameterAccessor accessor,
+ MongoParameters parameters, SpelExpressionParser expressionParser,
+ QueryMethodEvaluationContextProvider evaluationContextProvider) {
+
+ if (accessor.getCollation() != null) {
+ return query.collation(accessor.getCollation());
+ }
+
+ if (collationExpression == null) {
+ return query;
+ }
+
+ if (StringUtils.trimLeadingWhitespace(collationExpression).startsWith("{")) {
+
+ ParameterBindingContext bindingContext = new ParameterBindingContext((accessor::getBindableValue),
+ expressionParser, evaluationContextProvider.getEvaluationContext(parameters, accessor.getValues()));
+
+ return query.collation(Collation.from(CODEC.decode(collationExpression, bindingContext)));
+ }
+
+ Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(collationExpression);
+ if (!matcher.find()) {
+ return query.collation(Collation.parse(collationExpression));
+ }
+
+ String placeholder = matcher.group();
+ Object placeholderValue = accessor.getBindableValue(computeParameterIndex(placeholder));
+
+ if (collationExpression.startsWith("?")) {
+
+ if (placeholderValue instanceof String) {
+ return query.collation(Collation.parse(placeholderValue.toString()));
+ }
+ if (placeholderValue instanceof Locale) {
+ return query.collation(Collation.of((Locale) placeholderValue));
+ }
+ if (placeholderValue instanceof Document) {
+ return query.collation(Collation.from((Document) placeholderValue));
+ }
+ throw new IllegalArgumentException(String.format("Collation must be a String, Locale or Document but was %s",
+ ObjectUtils.nullSafeClassName(placeholderValue)));
+ }
+
+ return query.collation(Collation.parse(collationExpression.replace(placeholder, placeholderValue.toString())));
+ }
+
+ private static int computeParameterIndex(String parameter) {
+ return NumberUtils.parseNumber(parameter.replace("?", ""), Integer.class);
+ }
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java
index 3fd1fd253c..f5af831a7d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java
@@ -17,6 +17,7 @@
import org.bson.Document;
import org.bson.json.JsonParseException;
+
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -25,10 +26,12 @@
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.StringUtils;
/**
@@ -50,10 +53,13 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
+ * @param expressionParser must not be {@literal null}.
+ * @param evaluationContextProvider must not be {@literal null}.
*/
- public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations) {
+ public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
+ SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
- super(method, mongoOperations);
+ super(method, mongoOperations, expressionParser, evaluationContextProvider);
this.processor = method.getResultProcessor();
this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java
index 1850576e0e..5e1e73040c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java
@@ -18,6 +18,7 @@
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
@@ -78,7 +79,7 @@ public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod meth
ReactiveMongoOperations mongoOperations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
- super(method, mongoOperations);
+ super(method, mongoOperations, expressionParser, evaluationContextProvider);
Assert.notNull(query, "Query must not be null!");
Assert.notNull(expressionParser, "SpelExpressionParser must not be null!");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java
index 42a1c9fffc..7b045e8f98 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java
@@ -18,6 +18,7 @@
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
@@ -77,7 +78,7 @@ public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOpera
public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations,
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
- super(method, mongoOperations);
+ super(method, mongoOperations, expressionParser, evaluationContextProvider);
Assert.notNull(query, "Query must not be null!");
Assert.notNull(expressionParser, "SpelExpressionParser must not be null!");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java
index d470eecebd..62753898d6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java
@@ -21,12 +21,14 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.repository.query.MongoEntityMetadata;
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
import org.springframework.data.repository.core.support.QueryCreationListener;
@@ -93,6 +95,14 @@ public void onCreation(PartTreeMongoQuery query) {
}
}
+ if (query.getQueryMethod().hasAnnotatedCollation()) {
+
+ String collation = query.getQueryMethod().getAnnotatedCollation();
+ if (!collation.contains("?")) {
+ index = index.collation(Collation.parse(collation));
+ }
+ }
+
MongoEntityMetadata> metadata = query.getQueryMethod().getEntityInformation();
indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
LOG.debug(String.format("Created %s!", index));
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
index 594cd94870..9f81af2e1f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
@@ -18,6 +18,7 @@
import org.bson.types.ObjectId;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.repository.core.support.PersistentEntityInformation;
import org.springframework.lang.Nullable;
@@ -143,4 +144,13 @@ public Object getVersion(T entity) {
return accessor.getProperty(this.entityMetadata.getRequiredVersionProperty());
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.repository.MongoEntityInformation#getCollation()
+ */
+ @Nullable
+ public Collation getCollation() {
+ return this.entityMetadata.getCollation();
+ }
+
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java
index 2920285d36..eeff22eb47 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java
@@ -190,7 +190,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
} else if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider);
} else {
- return new PartTreeMongoQuery(queryMethod, operations);
+ return new PartTreeMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider);
}
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java
index 0e24b1ea7b..c5001dcf23 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java
@@ -180,7 +180,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
} else if (queryMethod.hasAnnotatedQuery()) {
return new ReactiveStringBasedMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider);
} else {
- return new ReactivePartTreeMongoQuery(queryMethod, operations);
+ return new ReactivePartTreeMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider);
}
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java
index 678a7a0c2b..2ad380c785 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java
@@ -227,7 +227,7 @@ public Page findAll(Pageable pageable) {
Long count = count();
List list = findAll(new Query().with(pageable));
- return new PageImpl(list, pageable, count);
+ return new PageImpl<>(list, pageable, count);
}
/*
@@ -282,7 +282,9 @@ public Page findAll(final Example example, Pageable pageable
Assert.notNull(example, "Sample must not be null!");
Assert.notNull(pageable, "Pageable must not be null!");
- Query query = new Query(new Criteria().alike(example)).with(pageable);
+ Query query = new Query(new Criteria().alike(example)) //
+ .collation(entityInformation.getCollation()).with(pageable); //
+
List list = mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName());
return PageableExecutionUtils.getPage(list, pageable,
@@ -299,9 +301,11 @@ public List findAll(Example example, Sort sort) {
Assert.notNull(example, "Sample must not be null!");
Assert.notNull(sort, "Sort must not be null!");
- Query q = new Query(new Criteria().alike(example)).with(sort);
+ Query query = new Query(new Criteria().alike(example)) //
+ .collation(entityInformation.getCollation()) //
+ .with(sort);
- return mongoOperations.find(q, example.getProbeType(), entityInformation.getCollectionName());
+ return mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName());
}
/*
@@ -322,9 +326,11 @@ public Optional findOne(Example example) {
Assert.notNull(example, "Sample must not be null!");
- Query q = new Query(new Criteria().alike(example));
+ Query query = new Query(new Criteria().alike(example)) //
+ .collation(entityInformation.getCollation());
+
return Optional
- .ofNullable(mongoOperations.findOne(q, example.getProbeType(), entityInformation.getCollectionName()));
+ .ofNullable(mongoOperations.findOne(query, example.getProbeType(), entityInformation.getCollectionName()));
}
/*
@@ -336,8 +342,10 @@ public long count(Example example) {
Assert.notNull(example, "Sample must not be null!");
- Query q = new Query(new Criteria().alike(example));
- return mongoOperations.count(q, example.getProbeType(), entityInformation.getCollectionName());
+ Query query = new Query(new Criteria().alike(example)) //
+ .collation(entityInformation.getCollation());
+
+ return mongoOperations.count(query, example.getProbeType(), entityInformation.getCollectionName());
}
/*
@@ -349,8 +357,10 @@ public