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