diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java
index 1b1f3e1d3..bdaa8385a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java
@@ -118,6 +118,23 @@ public class Collation {
return new Collation(locale);
}
+ /**
+ * Parse the given collation string into a {@link Collation}.
+ *
+ * @param collation the collation to parse. Can be a simple string like {@code en_US} or a
+ * {@link Document#parse(String) parsable} document like { '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}.
*
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 8f902aead..7e2d4fbe4 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,36 @@ public @interface Query {
* @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);
+ *
+ *
+ *
+ * @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 4784b15f8..0a4258e72 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
@@ -86,6 +86,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
applyQueryMetaAttributesWhenPresent(query);
query = applyAnnotatedDefaultSortIfPresent(query);
+ query = applyAnnotatedCollationIfPresent(query, accessor);
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
Class> typeToRead = processor.getReturnedType().getTypeToRead();
@@ -154,6 +155,21 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
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);
+ }
+
/**
* 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 d5c341569..706a566f9 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
@@ -105,12 +105,14 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
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 +121,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
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 +197,21 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
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);
+ }
+
/**
* 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 f44e799e1..6d98f30bc 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.Distance;
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 class ConvertingParameterAccessor implements MongoParameterAccessor {
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/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java
index f41a1fa12..43bdfcc22 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 @@ package org.springframework.data.mongodb.repository.query;
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 5db8b24fb..e7800498a 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.core.MethodParameter;
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 class MongoParameters extends Parameters
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 class MongoParameters extends Parameters
}
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 @@ public class MongoParameters extends Parameters
this.fullTextIndex = fullTextIndex;
this.maxDistanceIndex = maxDistanceIndex;
this.rangeIndex = rangeIndex;
+ this.collationIndex = collationIndex;
}
private final int getNearIndex(List> parameterTypes) {
@@ -111,11 +115,11 @@ public class MongoParameters extends Parameters
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 @@ public class MongoParameters extends Parameters
return new MongoParameter(parameter);
}
-
-
public int getDistanceRangeIndex() {
return -1;
}
@@ -158,7 +160,7 @@ public class MongoParameters extends Parameters
}
/**
- * 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 class MongoParameters extends Parameters
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 @@ public class MongoParameters extends Parameters
@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 3cf2e58bd..2364f6862 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;
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 class MongoParametersParameterAccessor extends ParametersParameterAccesso
}
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 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso
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 9bc7d00a1..84b120492 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 class MongoQueryMethod extends QueryMethod {
"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#sort()} 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/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
index c9cca1266..5bbafeec0 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,10 +15,18 @@
*/
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.lang.Nullable;
+import org.springframework.util.NumberUtils;
+import org.springframework.util.ObjectUtils;
/**
* Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Query} support
@@ -30,6 +38,8 @@ import org.springframework.data.mongodb.core.query.Query;
*/
class QueryUtils {
+ 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 +68,58 @@ class QueryUtils {
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) {
+
+ if (accessor.getCollation() != null) {
+ return query.collation(accessor.getCollation());
+ }
+
+ if (collationExpression == null) {
+ return query;
+ }
+
+ Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(collationExpression);
+
+ // TODO: use parameter binding Parser instead of Document.parse once DATAMONGO-2199 is merged.
+
+ 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/support/IndexEnsuringQueryCreationListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java
index d470eeceb..1706b419c 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
@@ -27,6 +27,7 @@ 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 +94,14 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener metadata = query.getQueryMethod().getEntityInformation();
indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
LOG.debug(String.format("Created %s!", index));
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
index 290103e4d..99a917319 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
@@ -15,8 +15,7 @@
*/
package org.springframework.data.mongodb.repository.query;
-import static org.hamcrest.Matchers.*;
-import static org.junit.Assert.*;
+import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -24,6 +23,7 @@ import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.List;
+import java.util.Locale;
import java.util.Optional;
import org.bson.Document;
@@ -52,6 +52,7 @@ import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.MongoRepository;
@@ -121,7 +122,7 @@ public class AbstractMongoQueryUnitTests {
MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class);
query.setDeleteQuery(true);
- assertThat(query.execute(new Object[] { "fake" }), is(0L));
+ assertThat(query.execute(new Object[] { "fake" })).isEqualTo(0L);
}
@Test // DATAMONGO-566, DATAMONGO-978
@@ -133,7 +134,7 @@ public class AbstractMongoQueryUnitTests {
MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class);
query.setDeleteQuery(true);
- assertThat(query.execute(new Object[] { "fake" }), is(100L));
+ assertThat(query.execute(new Object[] { "fake" })).isEqualTo(100L);
verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons"));
}
@@ -148,7 +149,8 @@ public class AbstractMongoQueryUnitTests {
verify(executableFind).as(Person.class);
verify(withQueryMock).matching(captor.capture());
- assertThat(captor.getValue().getMeta().getComment(), nullValue());
+ assertThat(captor.getValue().getMeta().getComment()).isNull();
+ ;
}
@Test // DATAMONGO-957
@@ -162,7 +164,7 @@ public class AbstractMongoQueryUnitTests {
verify(executableFind).as(Person.class);
verify(withQueryMock).matching(captor.capture());
- assertThat(captor.getValue().getMeta().getComment(), is("comment"));
+ assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment");
}
@Test // DATAMONGO-957
@@ -176,7 +178,7 @@ public class AbstractMongoQueryUnitTests {
verify(executableFind).as(Person.class);
verify(withQueryMock, atLeast(1)).matching(captor.capture());
- assertThat(captor.getValue().getMeta().getComment(), is("comment"));
+ assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment");
}
@Test // DATAMONGO-957, DATAMONGO-1783
@@ -190,7 +192,7 @@ public class AbstractMongoQueryUnitTests {
verify(executableFind).as(Person.class);
verify(withQueryMock).matching(captor.capture());
- assertThat(captor.getValue().getMeta().getComment(), is("comment"));
+ assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment");
}
@Test // DATAMONGO-1057
@@ -208,8 +210,8 @@ public class AbstractMongoQueryUnitTests {
verify(executableFind, times(2)).as(Person.class);
verify(withQueryMock, times(2)).matching(captor.capture());
- assertThat(captor.getAllValues().get(0).getSkip(), is(0L));
- assertThat(captor.getAllValues().get(1).getSkip(), is(10L));
+ assertThat(captor.getAllValues().get(0).getSkip()).isZero();
+ assertThat(captor.getAllValues().get(1).getSkip()).isEqualTo(10);
}
@Test // DATAMONGO-1057
@@ -227,8 +229,8 @@ public class AbstractMongoQueryUnitTests {
verify(executableFind, times(2)).as(Person.class);
verify(withQueryMock, times(2)).matching(captor.capture());
- assertThat(captor.getAllValues().get(0).getLimit(), is(11));
- assertThat(captor.getAllValues().get(1).getLimit(), is(11));
+ assertThat(captor.getAllValues().get(0).getLimit()).isEqualTo(11);
+ assertThat(captor.getAllValues().get(1).getLimit()).isEqualTo(11);
}
@Test // DATAMONGO-1057
@@ -247,8 +249,8 @@ public class AbstractMongoQueryUnitTests {
verify(withQueryMock, times(2)).matching(captor.capture());
Document expectedSortObject = new Document().append("bar", -1);
- assertThat(captor.getAllValues().get(0).getSortObject(), is(expectedSortObject));
- assertThat(captor.getAllValues().get(1).getSortObject(), is(expectedSortObject));
+ assertThat(captor.getAllValues().get(0).getSortObject()).isEqualTo(expectedSortObject);
+ assertThat(captor.getAllValues().get(1).getSortObject()).isEqualTo(expectedSortObject);
}
@Test // DATAMONGO-1080
@@ -260,7 +262,7 @@ public class AbstractMongoQueryUnitTests {
AbstractMongoQuery query = createQueryForMethod("findByLastname", String.class);
- assertThat(query.execute(new Object[] { "lastname" }), is(reference));
+ assertThat(query.execute(new Object[] { "lastname" })).isEqualTo(reference);
}
@Test // DATAMONGO-1865
@@ -272,7 +274,7 @@ public class AbstractMongoQueryUnitTests {
AbstractMongoQuery query = createQueryForMethod("findFirstByLastname", String.class).setLimitingQuery(true);
- assertThat(query.execute(new Object[] { "lastname" }), is(reference));
+ assertThat(query.execute(new Object[] { "lastname" })).isEqualTo(reference);
}
@Test // DATAMONGO-1872
@@ -294,7 +296,7 @@ public class AbstractMongoQueryUnitTests {
ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
- assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", 1))));
+ assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("age", 1));
}
@Test // DATAMONGO-1979
@@ -305,7 +307,127 @@ public class AbstractMongoQueryUnitTests {
ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
- assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", -1))));
+ assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("age", -1));
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyStaticAnnotatedCollation() {
+
+ createQueryForMethod("findWithCollationUsingSpimpleStringValueByFirstName", String.class) //
+ .execute(new Object[] { "dalinar" });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyStaticAnnotatedCollationAsDocument() {
+
+ createQueryForMethod("findWithCollationUsingDocumentByFirstName", String.class) //
+ .execute(new Object[] { "dalinar" });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyDynamicAnnotatedCollationAsString() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
+ .execute(new Object[] { "dalinar", "en_US" });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyDynamicAnnotatedCollationAsDocument() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
+ .execute(new Object[] { "dalinar", new Document("locale", "en_US") });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyDynamicAnnotatedCollationAsLocale() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
+ .execute(new Object[] { "dalinar", Locale.US });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1854
+ public void shouldThrowExceptionOnNonParsableCollation() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
+ .execute(new Object[] { "dalinar", 100 });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyDynamicAnnotatedCollationIn() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderInDocumentByFirstName", String.class, String.class) //
+ .execute(new Object[] { "dalinar", "en_US" });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyCollationParameter() {
+
+ Collation collation = Collation.of("en_US");
+ createQueryForMethod("findWithCollationParameterByFirstName", String.class, Collation.class) //
+ .execute(new Object[] { "dalinar", collation });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation()).contains(collation);
+ }
+
+ @Test // DATAMONGO-1854
+ public void collationParameterShouldOverrideAnnotation() {
+
+ Collation collation = Collation.of("de_AT");
+ createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
+ .execute(new Object[] { "dalinar", collation });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation()).contains(collation);
+ }
+
+ @Test // DATAMONGO-1854
+ public void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() {
+
+ createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
+ .execute(new Object[] { "dalinar", null });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
}
private MongoQueryFake createQueryForMethod(String methodName, Class>... paramTypes) {
@@ -400,6 +522,23 @@ public class AbstractMongoQueryUnitTests {
@org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }")
List findByAge(Integer age, Sort page);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "en_US")
+ List findWithCollationUsingSpimpleStringValueByFirstName(String firstname);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
+ List findWithCollationUsingDocumentByFirstName(String firstname);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "?1")
+ List findWithCollationUsingPlaceholderByFirstName(String firstname, Object collation);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1' }")
+ List findWithCollationUsingPlaceholderInDocumentByFirstName(String firstname, String collation);
+
+ List findWithCollationParameterByFirstName(String firstname, Collation collation);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
+ List findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation);
}
// DATAMONGO-1872
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java
new file mode 100644
index 000000000..22eab4989
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2019 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
+ *
+ * http://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.repository.query;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Locale;
+
+import org.bson.Document;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.data.mongodb.core.Person;
+import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery;
+import org.springframework.data.mongodb.core.ReactiveFindOperation.ReactiveFind;
+import org.springframework.data.mongodb.core.ReactiveMongoOperations;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
+import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
+
+/**
+ * @author Christoph Strobl
+ * @currentRead Way of Kings - Brandon Sanderson
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class AbstractReactiveMongoQueryUnitTests {
+
+ @Mock ReactiveMongoOperations mongoOperationsMock;
+ @Mock BasicMongoPersistentEntity> persitentEntityMock;
+ @Mock MongoMappingContext mappingContextMock;
+
+ @Mock ReactiveFind> executableFind;
+ @Mock FindWithQuery> withQueryMock;
+
+ @Before
+ public void setUp() {
+
+ doReturn("persons").when(persitentEntityMock).getCollection();
+ doReturn(persitentEntityMock).when(mappingContextMock).getPersistentEntity(Mockito.any(Class.class));
+ doReturn(persitentEntityMock).when(mappingContextMock).getRequiredPersistentEntity(Mockito.any(Class.class));
+ doReturn(Person.class).when(persitentEntityMock).getType();
+
+ MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContextMock);
+ converter.afterPropertiesSet();
+
+ doReturn(converter).when(mongoOperationsMock).getConverter();
+
+ doReturn(executableFind).when(mongoOperationsMock).query(any());
+ doReturn(withQueryMock).when(executableFind).as(any());
+ doReturn(withQueryMock).when(withQueryMock).matching(any());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyStaticAnnotatedCollation() {
+
+ createQueryForMethod("findWithCollationUsingSpimpleStringValueByFirstName", String.class) //
+ .execute(new Object[] { "dalinar" });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyStaticAnnotatedCollationAsDocument() {
+
+ createQueryForMethod("findWithCollationUsingDocumentByFirstName", String.class) //
+ .execute(new Object[] { "dalinar" });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyDynamicAnnotatedCollationAsString() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
+ .execute(new Object[] { "dalinar", "en_US" });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyDynamicAnnotatedCollationAsDocument() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
+ .execute(new Object[] { "dalinar", new Document("locale", "en_US") });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyDynamicAnnotatedCollationAsLocale() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
+ .execute(new Object[] { "dalinar", Locale.US });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1854
+ public void shouldThrowExceptionOnNonParsableCollation() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
+ .execute(new Object[] { "dalinar", 100 });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyDynamicAnnotatedCollationIn() {
+
+ createQueryForMethod("findWithCollationUsingPlaceholderInDocumentByFirstName", String.class, String.class) //
+ .execute(new Object[] { "dalinar", "en_US" });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldApplyCollationParameter() {
+
+ Collation collation = Collation.of("en_US");
+ createQueryForMethod("findWithCollationParameterByFirstName", String.class, Collation.class) //
+ .execute(new Object[] { "dalinar", collation });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation()).contains(collation);
+ }
+
+ @Test // DATAMONGO-1854
+ public void collationParameterShouldOverrideAnnotation() {
+
+ Collation collation = Collation.of("de_AT");
+ createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
+ .execute(new Object[] { "dalinar", collation });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation()).contains(collation);
+ }
+
+ @Test // DATAMONGO-1854
+ public void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() {
+
+ createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
+ .execute(new Object[] { "dalinar", null });
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class);
+ verify(withQueryMock).matching(captor.capture());
+ assertThat(captor.getValue().getCollation().map(Collation::toDocument))
+ .contains(Collation.of("en_US").toDocument());
+ }
+
+ private ReactiveMongoQueryFake createQueryForMethod(String methodName, Class>... paramTypes) {
+ return createQueryForMethod(Repo.class, methodName, paramTypes);
+ }
+
+ private ReactiveMongoQueryFake createQueryForMethod(Class> repository, String methodName, Class>... paramTypes) {
+
+ try {
+
+ Method method = repository.getMethod(methodName, paramTypes);
+ ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
+ ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method,
+ new DefaultRepositoryMetadata(repository), factory, mappingContextMock);
+
+ return new ReactiveMongoQueryFake(queryMethod, mongoOperationsMock);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e.getMessage(), e);
+ }
+ }
+
+ private static class ReactiveMongoQueryFake extends AbstractReactiveMongoQuery {
+
+ private boolean isDeleteQuery;
+ private boolean isLimitingQuery;
+
+ public ReactiveMongoQueryFake(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations) {
+ super(method, operations);
+ }
+
+ @Override
+ protected Query createQuery(ConvertingParameterAccessor accessor) {
+ return new BasicQuery("{'foo':'bar'}");
+ }
+
+ @Override
+ protected boolean isCountQuery() {
+ return false;
+ }
+
+ @Override
+ protected boolean isExistsQuery() {
+ return false;
+ }
+
+ @Override
+ protected boolean isDeleteQuery() {
+ return isDeleteQuery;
+ }
+
+ @Override
+ protected boolean isLimiting() {
+ return isLimitingQuery;
+ }
+
+ public ReactiveMongoQueryFake setDeleteQuery(boolean isDeleteQuery) {
+ this.isDeleteQuery = isDeleteQuery;
+ return this;
+ }
+
+ public ReactiveMongoQueryFake setLimitingQuery(boolean limitingQuery) {
+
+ isLimitingQuery = limitingQuery;
+ return this;
+ }
+ }
+
+ private interface Repo extends ReactiveMongoRepository {
+
+ @org.springframework.data.mongodb.repository.Query(collation = "en_US")
+ List findWithCollationUsingSpimpleStringValueByFirstName(String firstname);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
+ List findWithCollationUsingDocumentByFirstName(String firstname);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "?1")
+ List findWithCollationUsingPlaceholderByFirstName(String firstname, Object collation);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1' }")
+ List findWithCollationUsingPlaceholderInDocumentByFirstName(String firstname, String collation);
+
+ List findWithCollationParameterByFirstName(String firstname, Collation collation);
+
+ @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
+ List findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation);
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java
index c54bbd506..a39b6a063 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java
@@ -15,15 +15,12 @@
*/
package org.springframework.data.mongodb.repository.query;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.hamcrest.CoreMatchers.*;
-import static org.junit.Assert.assertThat;
+import static org.assertj.core.api.Assertions.*;
import java.lang.reflect.Method;
import java.util.List;
import org.bson.Document;
-import org.hamcrest.core.IsNull;
import org.junit.Test;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
@@ -31,6 +28,7 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.projection.ProjectionFactory;
@@ -82,7 +80,7 @@ public class MongoParametersParameterAccessorUnitTests {
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod,
new Object[] { new Point(10, 20), DISTANCE });
- assertThat(accessor.getFullText(), IsNull.nullValue());
+ assertThat(accessor.getFullText()).isNull();
}
@Test // DATAMONGO-973
@@ -93,8 +91,8 @@ public class MongoParametersParameterAccessorUnitTests {
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod,
new Object[] { "spring", TextCriteria.forDefaultLanguage().matching("data") });
- assertThat(accessor.getFullText().getCriteriaObject().toJson(),
- equalTo(Document.parse("{ \"$text\" : { \"$search\" : \"data\"}}").toJson()));
+ assertThat(accessor.getFullText().getCriteriaObject().toJson())
+ .isEqualTo(Document.parse("{ \"$text\" : { \"$search\" : \"data\"}}").toJson());
}
@Test // DATAMONGO-1110
@@ -111,8 +109,21 @@ public class MongoParametersParameterAccessorUnitTests {
Range range = accessor.getDistanceRange();
- assertThat(range.getLowerBound(), is(Bound.inclusive(min)));
- assertThat(range.getUpperBound(), is(Bound.inclusive(max)));
+ assertThat(range.getLowerBound()).isEqualTo(Bound.inclusive(min));
+ assertThat(range.getUpperBound()).isEqualTo(Bound.inclusive(max));
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldDetectCollation() throws NoSuchMethodException, SecurityException {
+
+ Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Collation.class);
+ MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
+
+ Collation collation = Collation.of("en_US");
+ MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod,
+ new Object[] { "dalinar", collation });
+
+ assertThat(accessor.getCollation()).isEqualTo(collation);
}
interface PersonRepository extends Repository {
@@ -124,5 +135,8 @@ public class MongoParametersParameterAccessorUnitTests {
List findByLocationNear(Point point, Range distances);
List findByFirstname(String firstname, TextCriteria fullText);
+
+ List findByFirstname(String firstname, Collation collation);
+
}
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java
index 9081dc74e..9ad082660 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java
@@ -15,8 +15,7 @@
*/
package org.springframework.data.mongodb.repository.query;
-import static org.hamcrest.CoreMatchers.*;
-import static org.junit.Assert.*;
+import static org.assertj.core.api.Assertions.*;
import java.lang.reflect.Method;
import java.util.List;
@@ -29,6 +28,7 @@ import org.springframework.data.domain.Range;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
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.Person;
@@ -47,17 +47,18 @@ public class MongoParametersUnitTests {
@Test
public void discoversDistanceParameter() throws NoSuchMethodException, SecurityException {
+
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
MongoParameters parameters = new MongoParameters(method, false);
- assertThat(parameters.getNumberOfParameters(), is(2));
- assertThat(parameters.getMaxDistanceIndex(), is(1));
- assertThat(parameters.getBindableParameters().getNumberOfParameters(), is(1));
+ assertThat(parameters.getNumberOfParameters()).isEqualTo(2);
+ assertThat(parameters.getMaxDistanceIndex()).isEqualTo(1);
+ assertThat(parameters.getBindableParameters().getNumberOfParameters()).isOne();
Parameter parameter = parameters.getParameter(1);
- assertThat(parameter.isSpecialParameter(), is(true));
- assertThat(parameter.isBindable(), is(false));
+ assertThat(parameter.isSpecialParameter()).isTrue();
+ assertThat(parameter.isBindable()).isFalse();
}
@Test
@@ -65,39 +66,44 @@ public class MongoParametersUnitTests {
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
MongoParameters parameters = new MongoParameters(method, false);
- assertThat(parameters.getNearIndex(), is(-1));
+ assertThat(parameters.getNearIndex()).isEqualTo(-1);
}
@Test(expected = IllegalStateException.class)
public void rejectsMultiplePointsForGeoNearMethod() throws Exception {
+
Method method = PersonRepository.class.getMethod("findByLocationNearAndOtherLocation", Point.class, Point.class);
new MongoParameters(method, true);
}
@Test(expected = IllegalStateException.class)
public void rejectsMultipleDoubleArraysForGeoNearMethod() throws Exception {
+
Method method = PersonRepository.class.getMethod("invalidDoubleArrays", double[].class, double[].class);
new MongoParameters(method, true);
}
@Test
public void doesNotRejectMultiplePointsForSimpleQueryMethod() throws Exception {
+
Method method = PersonRepository.class.getMethod("someOtherMethod", Point.class, Point.class);
new MongoParameters(method, false);
}
@Test
public void findsAnnotatedPointForGeoNearQuery() throws Exception {
+
Method method = PersonRepository.class.getMethod("findByOtherLocationAndLocationNear", Point.class, Point.class);
MongoParameters parameters = new MongoParameters(method, true);
- assertThat(parameters.getNearIndex(), is(1));
+ assertThat(parameters.getNearIndex()).isOne();
}
@Test
public void findsAnnotatedDoubleArrayForGeoNearQuery() throws Exception {
+
Method method = PersonRepository.class.getMethod("validDoubleArrays", double[].class, double[].class);
MongoParameters parameters = new MongoParameters(method, true);
- assertThat(parameters.getNearIndex(), is(1));
+ assertThat(parameters.getNearIndex()).isOne();
}
@Test // DATAMONGO-973
@@ -105,7 +111,7 @@ public class MongoParametersUnitTests {
Method method = PersonRepository.class.getMethod("findByNameAndText", String.class, TextCriteria.class);
MongoParameters parameters = new MongoParameters(method, false);
- assertThat(parameters.getFullTextParameterIndex(), is(1));
+ assertThat(parameters.getFullTextParameterIndex()).isOne();
}
@Test // DATAMONGO-973
@@ -113,7 +119,7 @@ public class MongoParametersUnitTests {
Method method = PersonRepository.class.getMethod("findByNameAndText", String.class, TextCriteria.class);
MongoParameters parameters = new MongoParameters(method, false);
- assertThat(parameters.getParameter(parameters.getFullTextParameterIndex()).isSpecialParameter(), is(true));
+ assertThat(parameters.getParameter(parameters.getFullTextParameterIndex()).isSpecialParameter()).isTrue();
}
@Test // DATAMONGO-1110
@@ -122,19 +128,37 @@ public class MongoParametersUnitTests {
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Range.class);
MongoParameters parameters = new MongoParameters(method, false);
- assertThat(parameters.getRangeIndex(), is(1));
- assertThat(parameters.getMaxDistanceIndex(), is(-1));
+ assertThat(parameters.getRangeIndex()).isOne();
+ assertThat(parameters.getMaxDistanceIndex()).isEqualTo(-1);
}
@Test // DATAMONGO-1110
- public void shouldNotHaveMinDistanceIfOnlyOneDistanceParameterPresent() throws NoSuchMethodException,
- SecurityException {
+ public void shouldNotHaveMinDistanceIfOnlyOneDistanceParameterPresent()
+ throws NoSuchMethodException, SecurityException {
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
MongoParameters parameters = new MongoParameters(method, false);
- assertThat(parameters.getRangeIndex(), is(-1));
- assertThat(parameters.getMaxDistanceIndex(), is(1));
+ assertThat(parameters.getRangeIndex()).isEqualTo(-1);
+ assertThat(parameters.getMaxDistanceIndex()).isOne();
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldReturnMinusOneIfCollationParameterDoesNotExist() throws NoSuchMethodException, SecurityException {
+
+ Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
+ MongoParameters parameters = new MongoParameters(method, false);
+
+ assertThat(parameters.getCollationParameterIndex()).isEqualTo(-1);
+ }
+
+ @Test // DATAMONGO-1854
+ public void shouldReturnIndexOfCollationParameterIfExists() throws NoSuchMethodException, SecurityException {
+
+ Method method = PersonRepository.class.getMethod("findByText", String.class, Collation.class);
+ MongoParameters parameters = new MongoParameters(method, false);
+
+ assertThat(parameters.getCollationParameterIndex()).isOne();
}
interface PersonRepository {
@@ -154,5 +178,7 @@ public class MongoParametersUnitTests {
List findByNameAndText(String name, TextCriteria text);
List findByLocationNear(Point point, Range range);
+
+ List findByText(String text, Collation collation);
}
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java
index 28ebd223d..5672c2e91 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java
@@ -26,8 +26,10 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.convert.MongoWriter;
+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;
/**
* Simple {@link ParameterAccessor} that returns the given parameters unfiltered.
@@ -40,6 +42,7 @@ class StubParameterAccessor implements MongoParameterAccessor {
private final Object[] values;
private Range range = Range.unbounded();
+ private @Nullable Collation colllation;
/**
* Creates a new {@link ConvertingParameterAccessor} backed by a {@link StubParameterAccessor} simply returning the
@@ -63,6 +66,8 @@ class StubParameterAccessor implements MongoParameterAccessor {
this.range = (Range) value;
} else if (value instanceof Distance) {
this.range = Range.from(Bound. unbounded()).to(Bound.inclusive((Distance) value));
+ } else if (value instanceof Collation) {
+ this.colllation = Collation.class.cast(value);
}
}
}
@@ -133,6 +138,15 @@ class StubParameterAccessor implements MongoParameterAccessor {
return null;
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation()
+ */
+ @Override
+ public Collation getCollation() {
+ return this.colllation;
+ }
+
/* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues()
*/
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java
index 7f4b5205f..e5ad2c718 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java
@@ -15,18 +15,28 @@
*/
package org.springframework.data.mongodb.repository.support;
-import static org.mockito.ArgumentMatchers.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.anyString;
+import org.bson.Document;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.index.IndexDefinition;
+import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
+import org.springframework.data.mongodb.repository.query.MongoEntityMetadata;
+import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
import org.springframework.data.repository.query.parser.PartTree;
+import org.springframework.data.util.Streamable;
/**
* Unit tests for {@link IndexEnsuringQueryCreationListener}.
@@ -39,26 +49,109 @@ public class IndexEnsuringQueryCreationListenerUnitTests {
IndexEnsuringQueryCreationListener listener;
@Mock IndexOperationsProvider provider;
+ @Mock PartTree partTree;
+ @Mock PartTreeMongoQuery partTreeQuery;
+ @Mock MongoQueryMethod queryMethod;
+ @Mock IndexOperations indexOperations;
+ @Mock MongoEntityMetadata entityInformation;
@Before
public void setUp() {
+
this.listener = new IndexEnsuringQueryCreationListener(provider);
+
+ partTreeQuery = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS);
+ when(partTreeQuery.getTree()).thenReturn(partTree);
+ when(provider.indexOps(anyString())).thenReturn(indexOperations);
+ when(queryMethod.getEntityInformation()).thenReturn(entityInformation);
+ when(entityInformation.getCollectionName()).thenReturn("persons");
}
@Test // DATAMONGO-1753
public void skipsQueryCreationForMethodWithoutPredicate() {
- PartTree tree = mock(PartTree.class);
- when(tree.hasPredicate()).thenReturn(false);
+ when(partTree.hasPredicate()).thenReturn(false);
- PartTreeMongoQuery query = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS);
- when(query.getTree()).thenReturn(tree);
-
- listener.onCreation(query);
+ listener.onCreation(partTreeQuery);
verify(provider, times(0)).indexOps(any());
}
+ @Test // DATAMONGO-1854
+ public void usesCollationWhenPresentAndFixedValue() {
+
+ when(partTree.hasPredicate()).thenReturn(true);
+ when(partTree.getParts()).thenReturn(Streamable.empty());
+ when(partTree.getSort()).thenReturn(Sort.unsorted());
+ when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
+ when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
+ when(queryMethod.getAnnotatedCollation()).thenReturn("en_US");
+
+ listener.onCreation(partTreeQuery);
+
+ ArgumentCaptor indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
+ verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
+
+ IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
+ assertThat(indexDefinition.getIndexOptions()).isEqualTo(new Document("collation", new Document("locale", "en_US")));
+ }
+
+ @Test // DATAMONGO-1854
+ public void usesCollationWhenPresentAndFixedDocumentValue() {
+
+ when(partTree.hasPredicate()).thenReturn(true);
+ when(partTree.getParts()).thenReturn(Streamable.empty());
+ when(partTree.getSort()).thenReturn(Sort.unsorted());
+ when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
+ when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
+ when(queryMethod.getAnnotatedCollation()).thenReturn("{ 'locale' : 'en_US' }");
+
+ listener.onCreation(partTreeQuery);
+
+ ArgumentCaptor indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
+ verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
+
+ IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
+ assertThat(indexDefinition.getIndexOptions()).isEqualTo(new Document("collation", new Document("locale", "en_US")));
+ }
+
+ @Test // DATAMONGO-1854
+ public void skipsCollationWhenPresentButDynamic() {
+
+ when(partTree.hasPredicate()).thenReturn(true);
+ when(partTree.getParts()).thenReturn(Streamable.empty());
+ when(partTree.getSort()).thenReturn(Sort.unsorted());
+ when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
+ when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
+ when(queryMethod.getAnnotatedCollation()).thenReturn("{ 'locale' : '?0' }");
+
+ listener.onCreation(partTreeQuery);
+
+ ArgumentCaptor indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
+ verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
+
+ IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
+ assertThat(indexDefinition.getIndexOptions()).isEmpty();
+ }
+
+ @Test // DATAMONGO-1854
+ public void skipsCollationWhenNotPresent() {
+
+ when(partTree.hasPredicate()).thenReturn(true);
+ when(partTree.getParts()).thenReturn(Streamable.empty());
+ when(partTree.getSort()).thenReturn(Sort.unsorted());
+ when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
+ when(queryMethod.hasAnnotatedCollation()).thenReturn(false);
+
+ listener.onCreation(partTreeQuery);
+
+ ArgumentCaptor indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
+ verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
+
+ IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
+ assertThat(indexDefinition.getIndexOptions()).isEmpty();
+ }
+
interface SampleRepository {
Object findAllBy();
diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc
index 1a7913c8b..f22ef6a6c 100644
--- a/src/main/asciidoc/reference/mongodb.adoc
+++ b/src/main/asciidoc/reference/mongodb.adoc
@@ -1738,6 +1738,223 @@ WARNING: Indexes are only used if the collation used for the operation matches t
include::./mongo-json-schema.adoc[leveloffset=+1]
+<> support `Collations` via the `@org.springframework.data.mongodb.repository.Query` annotation.
+
+.Collation support for Repositories
+====
+[source,java]
+----
+public interface PersonRepository extends MongoRepository {
+
+ @Query(collation = "en_US") <1>
+ List findByFirstname(String firstname);
+
+ @Query(collation = "{ 'locale' : 'en_US' }") <2>
+ List findPersonByFirstname(String firstname);
+
+ @Query(collation = "?1") <3>
+ List findByFirstname(String firstname, Object collation);
+
+ @Query(collation = "{ 'locale' : '?1' }") <4>
+ List findByFirstname(String firstname, String collation);
+
+ List findByFirstname(String firstname, Collation collation); <5>
+
+ @Query(collation = "{ 'locale' : 'en_US' }")
+ List findByFirstname(String firstname, @Nullable Collation collation); <6>
+}
+----
+<1> Static collation definition resulting in `{ 'locale' : 'en_US' }`.
+<2> Static collation definition resulting in `{ 'locale' : 'en_US' }`.
+<3> Dynamic collation depending on 2nd method argument. Allowed types include `String` (eg. 'en_US'), `Locacle` (eg. Locacle.US)
+and `Document` (eg. new Document("locale", "en_US"))
+<4> Dynamic collation depending on 2nd method argument.
+<5> Apply the `Collation` method parameter to the query.
+<6> The `Collation` method parameter overrides the default `collation` from `@Query` if not null.
+
+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.
+====
+
+[[mongo.jsonSchema]]
+=== JSON Schema
+
+As of version 3.6, MongoDB supports collections that validate documents against a provided https://docs.mongodb.com/manual/core/schema-validation/#json-schema[JSON Schema].
+The schema itself and both validation action and level can be defined when creating the collection, as the following example shows:
+
+.Sample JSON schema
+====
+[source,json]
+----
+{
+ "type": "object", <1>
+
+ "required": [ "firstname", "lastname" ], <2>
+
+ "properties": { <3>
+
+ "firstname": { <4>
+ "type": "string",
+ "enum": [ "luke", "han" ]
+ },
+ "address": { <5>
+ "type": "object",
+ "properties": {
+ "postCode": { "type": "string", "minLength": 4, "maxLength": 5 }
+ }
+ }
+ }
+}
+----
+<1> JSON schema documents always describe a whole document from its root. A schema is a schema object itself that can contain
+embedded schema objects that describe properties and subdocuments.
+<2> `required` is a property that describes which properties are required in a document. It can be specified optionally, along with other
+schema constraints. See MongoDB's documentation on https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#available-keywords[available keywords].
+<3> `properties` is related to a schema object that describes an `object` type. It contains property-specific schema constraints.
+<4> `firstname` specifies constraints for the `firsname` field inside the document. Here, it is a string-based `properties` element declaring
+ possible field values.
+<5> `address` is a subdocument defining a schema for values in its `postCode` field.
+====
+
+You can provide a schema either by specifying a schema document (that is, by using the `Document` API to parse or build a document object) or by building it with Spring Data's JSON schema utilities in `org.springframework.data.mongodb.core.schema`. `MongoJsonSchema` is the entry point for all JSON schema-related operations. The following example shows how use `MongoJsonSchema.builder()` to create a JSON schema:
+
+.Creating a JSON schema
+====
+[source,java]
+----
+MongoJsonSchema.builder() <1>
+ .required("firstname", "lastname") <2>
+
+ .properties(
+ string("firstname").possibleValues("luke", "han"), <3>
+
+ object("address")
+ .properties(string("postCode").minLength(4).maxLength(5)))
+
+ .build(); <4>
+----
+<1> Obtain a schema builder to configure the schema with a fluent API.
+<2> Configure required properties.
+<3> Configure the String-typed `firstname` field, allowing only `luke` and `han` values. Properties can be typed or untyped. Use a static import of `JsonSchemaProperty` to make the syntax slightly more compact and to get entry points such as `string(…)`.
+<4> Build the schema object. Use the schema to create either a collection or <>.
+====
+
+There are already some predefined and strongly typed schema objects (`JsonSchemaObject` and `JsonSchemaProperty`) available
+through static methods on the gateway interfaces.
+However, you may need to build custom property validation rules, which can be created through the builder API, as the following example shows:
+
+[source,java]
+----
+// "birthdate" : { "bsonType": "date" }
+JsonSchemaProperty.named("birthdate").ofType(Type.dateType());
+
+// "birthdate" : { "bsonType": "date", "description", "Must be a date" }
+JsonSchemaProperty.named("birthdate").with(JsonSchemaObject.of(Type.dateType()).description("Must be a date"));
+----
+
+`CollectionOptions` provides the entry point to schema support for collections, as the following example shows:
+
+.Create collection with `$jsonSchema`
+====
+[source,java]
+----
+MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build();
+
+template.createCollection(Person.class, CollectionOptions.empty().schema(schema));
+----
+====
+
+You can use a schema to query any collection for documents that match a given structure defined by a JSON schema, as the following example shows:
+
+.Query for Documents matching a `$jsonSchema`
+====
+[source,java]
+----
+MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build();
+
+template.find(query(matchingDocumentStructure(schema)), Person.class);
+----
+====
+
+The following table shows the supported JSON schema types:
+
+[cols="3,1,6", options="header"]
+.Supported JSON schema types
+|===
+| Schema Type
+| Java Type
+| Schema Properties
+
+| `untyped`
+| -
+| `description`, generated `description`, `enum`, `allOf`, `anyOf`, `oneOf`, `not`
+
+| `object`
+| `Object`
+| `required`, `additionalProperties`, `properties`, `minProperties`, `maxProperties`, `patternProperties`
+
+| `array`
+| any array except `byte[]`
+| `uniqueItems`, `additionalItems`, `items`, `minItems`, `maxItems`
+
+| `string`
+| `String`
+| `minLength`, `maxLentgth`, `pattern`
+
+| `int`
+| `int`, `Integer`
+| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
+
+| `long`
+| `long`, `Long`
+| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
+
+| `double`
+| `float`, `Float`, `double`, `Double`
+| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
+
+| `decimal`
+| `BigDecimal`
+| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
+
+| `number`
+| `Number`
+| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
+
+| `binData`
+| `byte[]`
+| (none)
+
+| `boolean`
+| `boolean`, `Boolean`
+| (none)
+
+| `null`
+| `null`
+| (none)
+
+| `objectId`
+| `ObjectId`
+| (none)
+
+| `date`
+| `java.util.Date`
+| (none)
+
+| `timestamp`
+| `BsonTimestamp`
+| (none)
+
+| `regex`
+| `java.util.regex.Pattern`
+| (none)
+
+|===
+
+NOTE: `untyped` is a generic type that is inherited by all typed schema types. It provides all `untyped` schema properties to typed schema types.
+
+For more information, see https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#op._S_jsonSchema[$jsonSchema].
+
[[mongo.query.fluent-template-api]]
=== Fluent Template API