DATAMONGO-1854 - Add collation option to @Document and @Query annotation.

We now allow to specify the collation via the @Query annotation.

public interface PersonRepository extends MongoRepository<Person, String> {

	@Query(collation = "en_US")
	List<Person> findByFirstname(String firstname);

	@Query(collation = "{ 'locale' : 'en_US' }")
	List<Person> findPersonByFirstname(String firstname);

	@Query(collation = "?1")
	List<Person> findByFirstname(String firstname, Object collation);

	@Query(collation = "{ 'locale' : '?1' }")
	List<Person> findByFirstname(String firstname, String collation);

	List<Person> findByFirstname(String firstname, Collation collation);
}

We now make sure to include collation information derived from the Query method if the collation is a fixed value.

Original pull request: #644.
This commit is contained in:
Christoph Strobl
2019-02-18 10:01:25 +01:00
committed by Mark Paluch
parent 5c333d6159
commit b8368a677d
18 changed files with 1082 additions and 66 deletions

View File

@@ -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 <code>&#123; 'locale' : '?0' &#125;</code> .
* @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}.
*

View File

@@ -95,4 +95,36 @@ public @interface Query {
* @since 2.1
*/
String sort() default "";
/**
* Defines the collation to apply when executing the query. <br />
*
* <pre>
* <code>
*
* // Fixed value
* &#64;Query(collation = "en_US")
* List<Entry> findAllByFixedCollation();
*
* // Fixed value as Document
* &#64;Query(collation = "{ 'locale' : 'en_US' }")
* List<Entry> findAllByFixedJsonCollation();
*
* // Dynamic value as String
* &#64;Query(collation = "?0")
* List<Entry> findAllByDynamicCollation(String collation);
*
* // Dynamic value as Document
* &#64;Query(collation = "{ 'locale' : ?0 }")
* List<Entry> findAllByDynamicJsonCollation(String collation);
*
* // SpEL expression
* &#64;Query(collation = "?#{[0]}")
* List<Entry> findAllByDynamicSpElCollation(String collation);
* </code>
* </pre>
*
* @since 2.2
*/
String collation() default "";
}

View File

@@ -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

View File

@@ -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

View File

@@ -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}.
*

View File

@@ -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.
*

View File

@@ -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<MongoParameters, MongoParameter>
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<MongoParameters, MongoParameter>
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<MongoParameters, MongoParameter>
}
private MongoParameters(List<MongoParameter> 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<MongoParameters, MongoParameter>
this.fullTextIndex = fullTextIndex;
this.maxDistanceIndex = maxDistanceIndex;
this.rangeIndex = rangeIndex;
this.collationIndex = collationIndex;
}
private final int getNearIndex(List<Class<?>> parameterTypes) {
@@ -111,11 +115,11 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
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<MongoParameters, MongoParameter>
return new MongoParameter(parameter);
}
public int getDistanceRangeIndex() {
return -1;
}
@@ -158,7 +160,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
}
/**
* 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<MongoParameters, MongoParameter>
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<MongoParameter> 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<TypeInformation<?>> parameterTypes, Class<?> type, @Nullable Class<?> componentType) {
@@ -241,7 +254,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
@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() {

View File

@@ -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<Distance> maxDistance = maxDistanceIndex == -1 ? Bound.unbounded() : Bound.inclusive((Distance) getValue(maxDistanceIndex));
Bound<Distance> 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()

View File

@@ -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 <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {

View File

@@ -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);
}
}

View File

@@ -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<PartTr
}
}
if (query.getQueryMethod().hasAnnotatedCollation()) {
String collation = query.getQueryMethod().getAnnotatedCollation();
if (!collation.contains("?")) {
index = index.collation(Collation.parse(collation));
}
}
MongoEntityMetadata<?> metadata = query.getQueryMethod().getEntityInformation();
indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
LOG.debug(String.format("Created %s!", index));

View File

@@ -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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Person> findByAge(Integer age, Sort page);
@org.springframework.data.mongodb.repository.Query(collation = "en_US")
List<Person> findWithCollationUsingSpimpleStringValueByFirstName(String firstname);
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
List<Person> findWithCollationUsingDocumentByFirstName(String firstname);
@org.springframework.data.mongodb.repository.Query(collation = "?1")
List<Person> findWithCollationUsingPlaceholderByFirstName(String firstname, Object collation);
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1' }")
List<Person> findWithCollationUsingPlaceholderInDocumentByFirstName(String firstname, String collation);
List<Person> findWithCollationParameterByFirstName(String firstname, Collation collation);
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
List<Person> findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation);
}
// DATAMONGO-1872

View File

@@ -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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Query> 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<Person, Long> {
@org.springframework.data.mongodb.repository.Query(collation = "en_US")
List<Person> findWithCollationUsingSpimpleStringValueByFirstName(String firstname);
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
List<Person> findWithCollationUsingDocumentByFirstName(String firstname);
@org.springframework.data.mongodb.repository.Query(collation = "?1")
List<Person> findWithCollationUsingPlaceholderByFirstName(String firstname, Object collation);
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1' }")
List<Person> findWithCollationUsingPlaceholderInDocumentByFirstName(String firstname, String collation);
List<Person> findWithCollationParameterByFirstName(String firstname, Collation collation);
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
List<Person> findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation);
}
}

View File

@@ -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<Distance> 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<Person, Long> {
@@ -124,5 +135,8 @@ public class MongoParametersParameterAccessorUnitTests {
List<Person> findByLocationNear(Point point, Range<Distance> distances);
List<Person> findByFirstname(String firstname, TextCriteria fullText);
List<Person> findByFirstname(String firstname, Collation collation);
}
}

View File

@@ -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<Person> findByNameAndText(String name, TextCriteria text);
List<Person> findByLocationNear(Point point, Range<Distance> range);
List<Person> findByText(String text, Collation collation);
}
}

View File

@@ -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<Distance> 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<Distance>) value;
} else if (value instanceof Distance) {
this.range = Range.from(Bound.<Distance> 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()
*/

View File

@@ -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<IndexDefinition> 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<IndexDefinition> 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<IndexDefinition> 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<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
assertThat(indexDefinition.getIndexOptions()).isEmpty();
}
interface SampleRepository {
Object findAllBy();

View File

@@ -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]
<<mongo.repositories>> support `Collations` via the `@org.springframework.data.mongodb.repository.Query` annotation.
.Collation support for Repositories
====
[source,java]
----
public interface PersonRepository extends MongoRepository<Person, String> {
@Query(collation = "en_US") <1>
List<Person> findByFirstname(String firstname);
@Query(collation = "{ 'locale' : 'en_US' }") <2>
List<Person> findPersonByFirstname(String firstname);
@Query(collation = "?1") <3>
List<Person> findByFirstname(String firstname, Object collation);
@Query(collation = "{ 'locale' : '?1' }") <4>
List<Person> findByFirstname(String firstname, String collation);
List<Person> findByFirstname(String firstname, Collation collation); <5>
@Query(collation = "{ 'locale' : 'en_US' }")
List<Person> 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 <<mongodb-template-query.criteria,query documents>>.
====
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