DATAMONGO-1894 - Use reactive SpEL extensions for SpEL evaluation in query execution.

Original Pull Request: #874
This commit is contained in:
Mark Paluch
2020-06-30 12:12:44 +02:00
committed by Christoph Strobl
parent 00aaf2145b
commit 66fae82798
20 changed files with 517 additions and 156 deletions

View File

@@ -15,11 +15,11 @@
*/
package org.springframework.data.mongodb.repository.query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.bson.Document;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.mongodb.core.MongoOperations;
@@ -33,7 +33,7 @@ import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecu
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingConverter;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingExecution;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.util.TypeInformation;
@@ -55,7 +55,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
private final EntityInstantiators instantiators;
private final FindWithProjection<?> findOperationWithProjection;
private final SpelExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and
@@ -67,12 +67,12 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* @param evaluationContextProvider must not be {@literal null}.
*/
public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations,
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
SpelExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(method, "MongoQueryMethod must not be null!");
Assert.notNull(operations, "ReactiveMongoOperations must not be null!");
Assert.notNull(expressionParser, "SpelExpressionParser must not be null!");
Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!");
Assert.notNull(evaluationContextProvider, "ReactiveEvaluationContextExtension must not be null!");
this.method = method;
this.operations = operations;
@@ -98,25 +98,21 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
*/
public Object execute(Object[] parameters) {
public Publisher<Object> execute(Object[] parameters) {
return method.hasReactiveWrapperParameter() ? executeDeferred(parameters)
: execute(new MongoParametersParameterAccessor(method, parameters));
}
@SuppressWarnings("unchecked")
private Object executeDeferred(Object[] parameters) {
private Publisher<Object> executeDeferred(Object[] parameters) {
ReactiveMongoParameterAccessor parameterAccessor = new ReactiveMongoParameterAccessor(method, parameters);
if (getQueryMethod().isCollectionQuery()) {
return Flux.defer(() -> (Publisher<Object>) execute(parameterAccessor));
}
return Mono.defer(() -> (Mono<Object>) execute(parameterAccessor));
return execute(parameterAccessor);
}
private Object execute(MongoParameterAccessor parameterAccessor) {
private Publisher<Object> execute(MongoParameterAccessor parameterAccessor) {
ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(operations.getConverter(),
parameterAccessor);
@@ -141,24 +137,27 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* @param accessor for providing invocation arguments. Never {@literal null}.
* @param typeToRead the desired component target type. Can be {@literal null}.
*/
protected Object doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor,
protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor,
ConvertingParameterAccessor accessor, @Nullable Class<?> typeToRead) {
Query query = createQuery(accessor);
applyQueryMetaAttributesWhenPresent(query);
query = applyAnnotatedDefaultSortIfPresent(query);
query = applyAnnotatedCollationIfPresent(query, accessor);
return createQuery(accessor).flatMapMany(it -> {
FindWithQuery<?> find = typeToRead == null //
? findOperationWithProjection //
: findOperationWithProjection.as(typeToRead);
Query query = it;
applyQueryMetaAttributesWhenPresent(query);
query = applyAnnotatedDefaultSortIfPresent(query);
query = applyAnnotatedCollationIfPresent(query, accessor);
String collection = method.getEntityInformation().getCollectionName();
FindWithQuery<?> find = typeToRead == null //
? findOperationWithProjection //
: findOperationWithProjection.as(typeToRead);
ReactiveMongoQueryExecution execution = getExecution(accessor,
new ResultProcessingConverter(processor, operations, instantiators), find);
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
String collection = method.getEntityInformation().getCollectionName();
ReactiveMongoQueryExecution execution = getExecution(accessor,
new ResultProcessingConverter(processor, operations, instantiators), find);
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
});
}
/**
@@ -254,8 +253,8 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* @param accessor must not be {@literal null}.
* @return
*/
protected Query createCountQuery(ConvertingParameterAccessor accessor) {
return applyQueryMetaAttributesWhenPresent(createQuery(accessor));
protected Mono<Query> createCountQuery(ConvertingParameterAccessor accessor) {
return createQuery(accessor).map(this::applyQueryMetaAttributesWhenPresent);
}
/**
@@ -264,7 +263,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* @param accessor must not be {@literal null}.
* @return
*/
protected abstract Query createQuery(ConvertingParameterAccessor accessor);
protected abstract Mono<Query> createQuery(ConvertingParameterAccessor accessor);
/**
* Returns whether the query should get a count projection applied.

View File

@@ -72,8 +72,9 @@ abstract class CollationUtils {
if (StringUtils.trimLeadingWhitespace(collationExpression).startsWith("{")) {
ParameterBindingContext bindingContext = new ParameterBindingContext((accessor::getBindableValue),
expressionParser, () -> evaluationContextProvider.getEvaluationContext(parameters, accessor.getValues()));
ParameterBindingContext bindingContext = ParameterBindingContext.forExpressions(accessor::getBindableValue,
expressionParser, dependencies -> evaluationContextProvider.getEvaluationContext(parameters,
accessor.getValues(), dependencies));
return Collation.from(CODEC.decode(collationExpression, bindingContext));
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.query;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
/**
* Simple {@link SpELExpressionEvaluator} implementation using {@link ExpressionParser} and {@link EvaluationContext}.
*
* @author Mark Paluch
* @since 3.1
*/
class DefaultSpELExpressionEvaluator implements SpELExpressionEvaluator {
private final ExpressionParser parser;
private final EvaluationContext context;
DefaultSpELExpressionEvaluator(ExpressionParser parser, EvaluationContext context) {
this.parser = parser;
this.context = context;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.SpELExpressionEvaluator#evaluate(java.lang.String)
*/
@Override
@SuppressWarnings("unchecked")
public <T> T evaluate(String expression) {
return (T) parser.parseExpression(expression).getValue(context, Object.class);
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.query;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
/**
* {@link SpELExpressionEvaluator} that does not support SpEL evaluation.
*
* @author Mark Paluch
* @since 3.1
*/
enum NoOpExpressionEvaluator implements SpELExpressionEvaluator {
INSTANCE;
@Override
public <T> T evaluate(String expression) {
throw new UnsupportedOperationException("Expression evaluation not supported");
}
}

View File

@@ -48,7 +48,7 @@ import org.springframework.util.ClassUtils;
*/
interface ReactiveMongoQueryExecution {
Object execute(Query query, Class<?> type, String collection);
Publisher<? extends Object> execute(Query query, Class<?> type, String collection);
/**
* {@link MongoQueryExecution} to execute geo-near queries.
@@ -74,7 +74,7 @@ interface ReactiveMongoQueryExecution {
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String)
*/
@Override
public Object execute(Query query, Class<?> type, String collection) {
public Publisher<? extends Object> execute(Query query, Class<?> type, String collection) {
Flux<GeoResult<Object>> results = doExecuteQuery(query, type, collection);
return isStreamOfGeoResult() ? results : results.map(GeoResult::getContent);
@@ -132,7 +132,7 @@ interface ReactiveMongoQueryExecution {
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String)
*/
@Override
public Object execute(Query query, Class<?> type, String collection) {
public Publisher<? extends Object> execute(Query query, Class<?> type, String collection) {
if (method.isCollectionQuery()) {
return operations.findAllAndRemove(query, type, collection);
@@ -166,8 +166,8 @@ interface ReactiveMongoQueryExecution {
}
@Override
public Object execute(Query query, Class<?> type, String collection) {
return converter.convert(delegate.execute(query, type, collection));
public Publisher<? extends Object> execute(Query query, Class<?> type, String collection) {
return (Publisher) converter.convert(delegate.execute(query, type, collection));
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.repository.query;
import reactor.core.publisher.Mono;
import org.bson.Document;
import org.bson.json.JsonParseException;
@@ -26,7 +28,7 @@ import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
@@ -57,7 +59,7 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
* @param evaluationContextProvider must not be {@literal null}.
*/
public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
SpelExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
@@ -81,7 +83,7 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#createQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor, boolean)
*/
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
protected Mono<Query> createQuery(ConvertingParameterAccessor accessor) {
MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery);
Query query = creator.createQuery();
@@ -105,7 +107,7 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
returnedType.getInputProperties().forEach(query.fields()::include);
}
return query;
return Mono.just(query);
}
try {
@@ -113,7 +115,7 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
BasicQuery result = new BasicQuery(query.getQueryObject(), Document.parse(fieldSpec));
result.setSortObject(query.getSortObject());
return result;
return Mono.just(result);
} catch (JsonParseException o_O) {
throw new IllegalStateException(String.format("Invalid query or field specification in %s!", getQueryMethod()),
@@ -126,8 +128,8 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#createCountQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
*/
@Override
protected Query createCountQuery(ConvertingParameterAccessor accessor) {
return new MongoQueryCreator(tree, accessor, context, false).createQuery();
protected Mono<Query> createCountQuery(ConvertingParameterAccessor accessor) {
return Mono.fromSupplier(() -> new MongoQueryCreator(tree, accessor, context, false).createQuery());
}
/*

View File

@@ -16,11 +16,16 @@
package org.springframework.data.mongodb.repository.query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.bson.Document;
import org.reactivestreams.Publisher;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
@@ -29,8 +34,11 @@ import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.ClassUtils;
@@ -44,8 +52,10 @@ import org.springframework.util.ClassUtils;
*/
public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
private static final ParameterBindingDocumentCodec CODEC = new ParameterBindingDocumentCodec();
private final SpelExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final ReactiveMongoOperations reactiveMongoOperations;
private final MongoConverter mongoConverter;
@@ -57,7 +67,7 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
*/
public ReactiveStringBasedAggregation(ReactiveMongoQueryMethod method,
ReactiveMongoOperations reactiveMongoOperations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, reactiveMongoOperations, expressionParser, evaluationContextProvider);
@@ -72,52 +82,81 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#doExecute(org.springframework.data.mongodb.repository.query.ReactiveMongoQueryMethod, org.springframework.data.repository.query.ResultProcessor, org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor, java.lang.Class)
*/
@Override
protected Object doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor,
protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor,
ConvertingParameterAccessor accessor, Class<?> typeToRead) {
Class<?> sourceType = method.getDomainClass();
Class<?> targetType = typeToRead;
return computePipeline(accessor).flatMapMany(it -> {
List<AggregationOperation> pipeline = computePipeline(accessor);
AggregationUtils.appendSortIfPresent(pipeline, accessor, typeToRead);
AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor);
Class<?> sourceType = method.getDomainClass();
Class<?> targetType = typeToRead;
boolean isSimpleReturnType = isSimpleReturnType(typeToRead);
boolean isRawReturnType = ClassUtils.isAssignable(org.bson.Document.class, typeToRead);
List<AggregationOperation> pipeline = it;
if (isSimpleReturnType || isRawReturnType) {
targetType = Document.class;
}
AggregationUtils.appendSortIfPresent(pipeline, accessor, typeToRead);
AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor);
AggregationOptions options = computeOptions(method, accessor);
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline, options);
boolean isSimpleReturnType = isSimpleReturnType(typeToRead);
boolean isRawReturnType = ClassUtils.isAssignable(org.bson.Document.class, typeToRead);
Flux<?> flux = reactiveMongoOperations.aggregate(aggregation, targetType);
if (isSimpleReturnType || isRawReturnType) {
targetType = Document.class;
}
if (isSimpleReturnType && !isRawReturnType) {
flux = flux.handle((it, sink) -> {
AggregationOptions options = computeOptions(method, accessor);
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline, options);
Object result = AggregationUtils.extractSimpleTypeResult((Document) it, typeToRead, mongoConverter);
Flux<?> flux = reactiveMongoOperations.aggregate(aggregation, targetType);
if (result != null) {
sink.next(result);
}
});
}
if (isSimpleReturnType && !isRawReturnType) {
flux = flux.handle((item, sink) -> {
if (method.isCollectionQuery()) {
return flux;
} else {
return flux.next();
}
Object result = AggregationUtils.extractSimpleTypeResult((Document) item, typeToRead, mongoConverter);
if (result != null) {
sink.next(result);
}
});
}
if (method.isCollectionQuery()) {
return flux;
} else {
return flux.next();
}
});
}
private boolean isSimpleReturnType(Class<?> targetType) {
return MongoSimpleTypes.HOLDER.isSimpleType(targetType);
}
List<AggregationOperation> computePipeline(ConvertingParameterAccessor accessor) {
return AggregationUtils.computePipeline(getQueryMethod(), accessor, expressionParser, evaluationContextProvider);
private Mono<List<AggregationOperation>> computePipeline(ConvertingParameterAccessor accessor) {
MongoQueryMethod method = getQueryMethod();
List<Mono<AggregationOperation>> stages = new ArrayList<>(method.getAnnotatedAggregation().length);
for (String source : method.getAnnotatedAggregation()) {
Optional<ExpressionDependencies> dependencies = CODEC.getExpressionDependencies(source,
accessor::getBindableValue, expressionParser);
Mono<SpELExpressionEvaluator> evaluator = dependencies.map(
it -> evaluationContextProvider.getEvaluationContextLater(method.getParameters(), accessor.getValues(), it))
.map(evaluationContext -> evaluationContext
.map(it -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(expressionParser, it)))
.orElseGet(() -> Mono.just(NoOpExpressionEvaluator.INSTANCE));
Mono<AggregationOperation> stage = evaluator.map(it -> {
ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue, it);
return ctx -> ctx.getMappedObject(CODEC.decode(source, bindingContext), method.getDomainClass());
});
stages.add(stage);
}
return Flux.concat(stages).collectList();
}
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
@@ -136,7 +175,7 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#createQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
*/
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
protected Mono<Query> createQuery(ConvertingParameterAccessor accessor) {
throw new UnsupportedOperationException("No query support for aggregation");
}

View File

@@ -15,17 +15,25 @@
*/
package org.springframework.data.mongodb.repository.query;
import reactor.core.publisher.Mono;
import java.util.Optional;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
@@ -46,7 +54,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
private final String fieldSpec;
private final SpelExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final boolean isCountQuery;
private final boolean isExistsQuery;
@@ -62,13 +70,14 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
* @param evaluationContextProvider must not be {@literal null}.
*/
public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
SpelExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
this(method.getAnnotatedQuery(), method, mongoOperations, expressionParser, evaluationContextProvider);
}
/**
* Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link String}, {@link MongoQueryMethod},
* {@link MongoOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}.
* {@link MongoOperations}, {@link SpelExpressionParser} and
* {@link ReactiveExtensionAwareQueryMethodEvaluationContextProvider}.
*
* @param query must not be {@literal null}.
* @param method must not be {@literal null}.
@@ -77,7 +86,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
*/
public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method,
ReactiveMongoOperations mongoOperations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
@@ -114,21 +123,39 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#createQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
*/
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
protected Mono<Query> createQuery(ConvertingParameterAccessor accessor) {
ParameterBindingContext bindingContext = new ParameterBindingContext((accessor::getBindableValue), expressionParser,
() -> evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), accessor.getValues()));
Mono<Document> queryObject = getBindingContext(accessor, expressionParser, this.query)
.map(it -> CODEC.decode(this.query, it));
Mono<Document> fieldsObject = getBindingContext(accessor, expressionParser, this.fieldSpec)
.map(it -> CODEC.decode(this.fieldSpec, it));
Document queryObject = CODEC.decode(this.query, bindingContext);
Document fieldsObject = CODEC.decode(this.fieldSpec, bindingContext);
return queryObject.zipWith(fieldsObject).map(tuple -> {
Query query = new BasicQuery(queryObject, fieldsObject).with(accessor.getSort());
Query query = new BasicQuery(tuple.getT1(), tuple.getT2()).with(accessor.getSort());
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Created query %s for %s fields.", query.getQueryObject(), query.getFieldsObject()));
}
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Created query %s for %s fields.", query.getQueryObject(), query.getFieldsObject()));
}
return query;
return query;
});
}
private Mono<ParameterBindingContext> getBindingContext(ConvertingParameterAccessor accessor,
ExpressionParser expressionParser, String json) {
Optional<ExpressionDependencies> dependencies = CODEC.getExpressionDependencies(json, accessor::getBindableValue,
expressionParser);
Mono<SpELExpressionEvaluator> evaluator = dependencies
.map(it -> evaluationContextProvider.getEvaluationContextLater(getQueryMethod().getParameters(),
accessor.getValues(), it))
.map(evaluationContext -> evaluationContext
.map(it -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(expressionParser, it)))
.orElseGet(() -> Mono.just(NoOpExpressionEvaluator.INSTANCE));
return evaluator.map(it -> new ParameterBindingContext(accessor::getBindableValue, it));
}
/*

View File

@@ -15,16 +15,22 @@
*/
package org.springframework.data.mongodb.repository.query;
import java.util.Optional;
import org.bson.Document;
import org.bson.codecs.configuration.CodecRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
@@ -122,11 +128,8 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
ParameterBindingContext bindingContext = new ParameterBindingContext((accessor::getBindableValue), expressionParser,
() -> evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), accessor.getValues()));
Document queryObject = codec.decode(this.query, bindingContext);
Document fieldsObject = codec.decode(this.fieldSpec, bindingContext);
Document queryObject = codec.decode(this.query, getBindingContext(accessor, expressionParser, this.query));
Document fieldsObject = codec.decode(this.fieldSpec, getBindingContext(accessor, expressionParser, this.fieldSpec));
Query query = new BasicQuery(queryObject, fieldsObject).with(accessor.getSort());
@@ -137,6 +140,22 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
return query;
}
private ParameterBindingContext getBindingContext(ConvertingParameterAccessor accessor,
ExpressionParser expressionParser, String json) {
Optional<ExpressionDependencies> dependencies = codec.getExpressionDependencies(json, accessor::getBindableValue,
expressionParser);
SpELExpressionEvaluator evaluator = dependencies
.map(it -> evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(),
accessor.getValues(), it))
.map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(expressionParser,
evaluationContext))
.orElse(NoOpExpressionEvaluator.INSTANCE);
return new ParameterBindingContext(accessor::getBindableValue, evaluator);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isCountQuery()

View File

@@ -42,6 +42,7 @@ import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
@@ -127,7 +128,8 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new MongoQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
return Optional.of(new MongoQueryLookupStrategy(operations,
(ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider, mappingContext));
}
/*
@@ -157,11 +159,11 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
private static class MongoQueryLookupStrategy implements QueryLookupStrategy {
private final ReactiveMongoOperations operations;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
MongoQueryLookupStrategy(ReactiveMongoOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.operations = operations;

View File

@@ -16,13 +16,17 @@
package org.springframework.data.mongodb.repository.support;
import java.io.Serializable;
import java.util.Optional;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.index.IndexOperationsAdapter;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -84,10 +88,7 @@ public class ReactiveMongoRepositoryFactoryBean<T extends Repository<S, ID>, S,
/*
* (non-Javadoc)
*
* @see
* org.springframework.data.repository.support.RepositoryFactoryBeanSupport
* #createRepositoryFactory()
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createRepositoryFactory()
*/
@Override
protected RepositoryFactorySupport createRepositoryFactory() {
@@ -102,6 +103,16 @@ public class ReactiveMongoRepositoryFactoryBean<T extends Repository<S, ID>, S,
return factory;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createDefaultQueryMethodEvaluationContextProvider(ListableBeanFactory)
*/
@Override
protected Optional<QueryMethodEvaluationContextProvider> createDefaultQueryMethodEvaluationContextProvider(
ListableBeanFactory beanFactory) {
return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory));
}
/**
* Creates and initializes a {@link RepositoryFactorySupport} instance.
*
@@ -114,10 +125,7 @@ public class ReactiveMongoRepositoryFactoryBean<T extends Repository<S, ID>, S,
/*
* (non-Javadoc)
*
* @see
* org.springframework.data.repository.support.RepositoryFactoryBeanSupport
* #afterPropertiesSet()
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {

View File

@@ -15,8 +15,11 @@
*/
package org.springframework.data.mongodb.util.json;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
@@ -28,25 +31,21 @@ import org.springframework.lang.Nullable;
* To be used along with {@link ParameterBindingDocumentCodec#decode(String, ParameterBindingContext)}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.2
*/
public class ParameterBindingContext {
private final ValueProvider valueProvider;
private final SpelExpressionParser expressionParser;
private final Lazy<EvaluationContext> evaluationContext;
private final SpELExpressionEvaluator expressionEvaluator;
/**
* @param valueProvider
* @param expressionParser
* @param evaluationContext
* @deprecated since 2.2.3 - Please use
* {@link #ParameterBindingContext(ValueProvider, SpelExpressionParser, Supplier)} instead.
*/
@Deprecated
public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser expressionParser,
EvaluationContext evaluationContext) {
this(valueProvider, expressionParser, () -> evaluationContext);
}
@@ -59,9 +58,48 @@ public class ParameterBindingContext {
public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser expressionParser,
Supplier<EvaluationContext> evaluationContext) {
this(valueProvider, new SpELExpressionEvaluator() {
@Override
public <T> T evaluate(String expressionString) {
return (T) expressionParser.parseExpression(expressionString).getValue(evaluationContext.get(), Object.class);
}
});
}
/**
* @param valueProvider
* @param expressionEvaluator
* @since 3.1
*/
public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvaluator expressionEvaluator) {
this.valueProvider = valueProvider;
this.expressionParser = expressionParser;
this.evaluationContext = evaluationContext instanceof Lazy ? (Lazy) evaluationContext : Lazy.of(evaluationContext);
this.expressionEvaluator = expressionEvaluator;
}
/**
* Create a new {@link ParameterBindingContext} that is capable of expression parsing and can provide a
* {@link EvaluationContext} based on {@link ExpressionDependencies}.
*
* @param valueProvider
* @param expressionParser
* @param contextFunction
* @return
* @since 3.1
*/
public static ParameterBindingContext forExpressions(ValueProvider valueProvider,
SpelExpressionParser expressionParser, Function<ExpressionDependencies, EvaluationContext> contextFunction) {
return new ParameterBindingContext(valueProvider, new SpELExpressionEvaluator() {
@Override
public <T> T evaluate(String expressionString) {
Expression expression = expressionParser.parseExpression(expressionString);
ExpressionDependencies dependencies = ExpressionDependencies.discover(expression);
EvaluationContext evaluationContext = contextFunction.apply(dependencies);
return (T) expression.getValue(evaluationContext, Object.class);
}
});
}
@Nullable
@@ -71,17 +109,7 @@ public class ParameterBindingContext {
@Nullable
public Object evaluateExpression(String expressionString) {
Expression expression = expressionParser.parseExpression(expressionString);
return expression.getValue(getEvaluationContext(), Object.class);
}
public EvaluationContext getEvaluationContext() {
return this.evaluationContext.get();
}
public SpelExpressionParser getExpressionParser() {
return expressionParser;
return expressionEvaluator.evaluate(expressionString);
}
public ValueProvider getValueProvider() {

View File

@@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.bson.AbstractBsonReader.State;
@@ -39,7 +40,11 @@ import org.bson.Transformer;
import org.bson.codecs.*;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.json.JsonParseException;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.NumberUtils;
@@ -163,7 +168,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
public Document decode(@Nullable String json, Object[] values) {
return decode(json, new ParameterBindingContext((index) -> values[index], new SpelExpressionParser(),
() -> EvaluationContextProvider.DEFAULT.getEvaluationContext(values)));
EvaluationContextProvider.DEFAULT.getEvaluationContext(values)));
}
public Document decode(@Nullable String json, ParameterBindingContext bindingContext) {
@@ -176,6 +181,51 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
return this.decode(reader, DecoderContext.builder().build());
}
/**
* Determine {@link ExpressionDependencies} from Expressions that are nested in the {@code json} content. Returns
* {@link Optional#empty()} if {@code json} is empty or of it does not contain any SpEL expressions.
*
* @param json
* @param expressionParser
* @return a {@link Optional} containing merged {@link ExpressionDependencies} object if expressions were found,
* otherwise {@link Optional#empty()}.
* @since 3.1
*/
public Optional<ExpressionDependencies> getExpressionDependencies(@Nullable String json, ValueProvider valueProvider,
ExpressionParser expressionParser) {
if (StringUtils.isEmpty(json)) {
return Optional.empty();
}
List<ExpressionDependencies> dependencies = new ArrayList<>();
ParameterBindingContext context = new ParameterBindingContext(valueProvider, new SpELExpressionEvaluator() {
@Override
public <T> T evaluate(String expression) {
dependencies.add(ExpressionDependencies.discover(expressionParser.parseExpression(expression)));
return (T) new Object();
}
});
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json, context);
this.decode(reader, DecoderContext.builder().build());
if (dependencies.isEmpty()) {
return Optional.empty();
}
ExpressionDependencies result = ExpressionDependencies.empty();
for (ExpressionDependencies dependency : dependencies) {
result = result.mergeWith(dependency);
}
return Optional.of(result);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Document decode(final BsonReader reader, final DecoderContext decoderContext) {
@@ -349,4 +399,5 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
reader.readEndArray();
return list;
}
}

View File

@@ -36,6 +36,7 @@ import org.bson.types.Decimal128;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.ObjectId;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;

View File

@@ -67,6 +67,7 @@ import org.springframework.data.mongodb.test.util.MongoTestUtils;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.mongodb.reactivestreams.client.MongoClient;
@@ -112,7 +113,7 @@ public class ReactiveMongoRepositoryTests {
factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class);
factory.setBeanClassLoader(beanFactory.getClass().getClassLoader());
factory.setBeanFactory(beanFactory);
factory.setEvaluationContextProvider(QueryMethodEvaluationContextProvider.DEFAULT);
factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
return factory;
}

View File

@@ -45,6 +45,7 @@ import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory;
import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ClassUtils;
@@ -86,7 +87,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class);
factory.setBeanClassLoader(classLoader);
factory.setBeanFactory(beanFactory);
factory.setEvaluationContextProvider(QueryMethodEvaluationContextProvider.DEFAULT);
factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
repository = factory.getRepository(ReactivePersonRepostitory.class);

View File

@@ -18,6 +18,9 @@ package org.springframework.data.mongodb.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Locale;
@@ -48,7 +51,7 @@ 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;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
@@ -85,13 +88,16 @@ class AbstractReactiveMongoQueryUnitTests {
doReturn(executableFind).when(mongoOperationsMock).query(any());
doReturn(withQueryMock).when(executableFind).as(any());
doReturn(withQueryMock).when(withQueryMock).matching(any(Query.class));
doReturn(Flux.empty()).when(withQueryMock).all();
doReturn(Mono.empty()).when(withQueryMock).first();
doReturn(Mono.empty()).when(withQueryMock).one();
}
@Test // DATAMONGO-1854
void shouldApplyStaticAnnotatedCollation() {
createQueryForMethod("findWithCollationUsingSpimpleStringValueByFirstName", String.class) //
.execute(new Object[] { "dalinar" });
.executeBlocking(new Object[] { "dalinar" });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -103,7 +109,7 @@ class AbstractReactiveMongoQueryUnitTests {
void shouldApplyStaticAnnotatedCollationAsDocument() {
createQueryForMethod("findWithCollationUsingDocumentByFirstName", String.class) //
.execute(new Object[] { "dalinar" });
.executeBlocking(new Object[] { "dalinar" });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -115,7 +121,7 @@ class AbstractReactiveMongoQueryUnitTests {
void shouldApplyDynamicAnnotatedCollationAsString() {
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
.execute(new Object[] { "dalinar", "en_US" });
.executeBlocking(new Object[] { "dalinar", "en_US" });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -127,7 +133,7 @@ class AbstractReactiveMongoQueryUnitTests {
void shouldApplyDynamicAnnotatedCollationAsDocument() {
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
.execute(new Object[] { "dalinar", new Document("locale", "en_US") });
.executeBlocking(new Object[] { "dalinar", new Document("locale", "en_US") });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -139,7 +145,7 @@ class AbstractReactiveMongoQueryUnitTests {
void shouldApplyDynamicAnnotatedCollationAsLocale() {
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
.execute(new Object[] { "dalinar", Locale.US });
.executeBlocking(new Object[] { "dalinar", Locale.US });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -152,7 +158,7 @@ class AbstractReactiveMongoQueryUnitTests {
assertThatIllegalArgumentException().isThrownBy(() -> {
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
.execute(new Object[] { "dalinar", 100 });
.executeBlocking(new Object[] { "dalinar", 100 });
});
}
@@ -160,7 +166,7 @@ class AbstractReactiveMongoQueryUnitTests {
void shouldApplyDynamicAnnotatedCollationIn() {
createQueryForMethod("findWithCollationUsingPlaceholderInDocumentByFirstName", String.class, String.class) //
.execute(new Object[] { "dalinar", "en_US" });
.executeBlocking(new Object[] { "dalinar", "en_US" });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -173,7 +179,7 @@ class AbstractReactiveMongoQueryUnitTests {
createQueryForMethod("findWithCollationUsingPlaceholdersInDocumentByFirstName", String.class, String.class,
int.class) //
.execute(new Object[] { "dalinar", "en_US", 2 });
.executeBlocking(new Object[] { "dalinar", "en_US", 2 });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -186,7 +192,7 @@ class AbstractReactiveMongoQueryUnitTests {
Collation collation = Collation.of("en_US");
createQueryForMethod("findWithCollationParameterByFirstName", String.class, Collation.class) //
.execute(new Object[] { "dalinar", collation });
.executeBlocking(new Object[] { "dalinar", collation });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -198,7 +204,7 @@ class AbstractReactiveMongoQueryUnitTests {
Collation collation = Collation.of("de_AT");
createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
.execute(new Object[] { "dalinar", collation });
.executeBlocking(new Object[] { "dalinar", collation });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -209,7 +215,7 @@ class AbstractReactiveMongoQueryUnitTests {
void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() {
createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
.execute(new Object[] { "dalinar", null });
.executeBlocking(new Object[] { "dalinar", null });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
@@ -242,12 +248,17 @@ class AbstractReactiveMongoQueryUnitTests {
private boolean isLimitingQuery;
ReactiveMongoQueryFake(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations) {
super(method, operations, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT);
super(method, operations, new SpelExpressionParser(),
ReactiveExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT);
}
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
return new BasicQuery("{'foo':'bar'}");
protected Mono<Query> createQuery(ConvertingParameterAccessor accessor) {
return Mono.just(new BasicQuery("{'foo':'bar'}"));
}
Object executeBlocking(Object[] parameters) {
return Flux.from(super.execute(parameters)).collectList().block();
}
@Override

View File

@@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import lombok.Value;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -55,6 +56,7 @@ import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
@@ -172,7 +174,7 @@ public class ReactiveStringBasedAggregationUnitTests {
ArgumentCaptor<TypedAggregation> aggregationCaptor = ArgumentCaptor.forClass(TypedAggregation.class);
ArgumentCaptor<Class> targetTypeCaptor = ArgumentCaptor.forClass(Class.class);
Object result = aggregation.execute(args);
Object result = Flux.from((Publisher) aggregation.execute(args)).blockLast();
verify(operations).aggregate(aggregationCaptor.capture(), targetTypeCaptor.capture());
@@ -186,7 +188,7 @@ public class ReactiveStringBasedAggregationUnitTests {
ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method,
new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext());
return new ReactiveStringBasedAggregation(queryMethod, operations, PARSER,
QueryMethodEvaluationContextProvider.DEFAULT);
ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
}
private List<Document> pipelineOf(AggregationInvocation invocation) {

View File

@@ -48,7 +48,10 @@ import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Base64Utils;
@@ -83,7 +86,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastname", String.class);
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : 'Matthews'}");
assertThat(query.getQueryObject()).isEqualTo(reference.getQueryObject());
@@ -101,7 +104,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
converter.write(address, dbObject);
dbObject.remove(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
Document queryObject = new Document("address", dbObject);
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery(queryObject);
@@ -136,7 +139,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByParameterizedCriteriaAndFields",
Document.class, Map.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor).block();
assertThat(query.getQueryObject())
.isEqualTo(new BasicQuery("{ \"firstname\": \"first\", \"lastname\": \"last\"}").getQueryObject());
@@ -150,7 +153,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithParametersInExpression", int.class,
int.class, int.class, int.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor).block();
assertThat(query.getQueryObject())
.isEqualTo(new BasicQuery("{$where: 'return this.date.getUTCMonth() == 3 && this.date.getUTCDay() == 4;'}")
@@ -164,7 +167,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
String.class, String.class);
ConvertingParameterAccessor parameterAccessor = StubParameterAccessor.getAccessor(converter, "key", "value");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(parameterAccessor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(parameterAccessor).block();
assertThat(query.getQueryObject()).isEqualTo(new Document().append("key", "value"));
}
@@ -175,7 +178,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews");
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithExpression", String.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : 'Matthews'}");
assertThat(query.getQueryObject()).isEqualTo(reference.getQueryObject());
@@ -188,7 +191,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithExpressionAndNestedObject",
boolean.class, String.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{ \"id\" : { \"$exists\" : true}}");
assertThat(query.getQueryObject()).isEqualTo(reference.getQueryObject());
@@ -201,7 +204,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithExpressionAndMultipleNestedObjects",
boolean.class, String.class, String.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery(
"{ \"id\" : { \"$exists\" : true} , \"foo\" : 42 , \"bar\" : { \"$exists\" : false}}");
@@ -215,21 +218,43 @@ public class ReactiveStringBasedMongoQueryUnitTests {
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, binaryData);
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameAsBinary", byte[].class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery(
"{'lastname' : { '$binary' : '" + Base64Utils.encodeToString(binaryData) + "', '$type' : '" + 0 + "'}}");
assertThat(query.getQueryObject().toJson()).isEqualTo(reference.getQueryObject().toJson());
}
@Test // DATAMONGO-1894
void shouldConsiderReactiveSpelExtension() throws Exception {
ReactiveExtensionAwareQueryMethodEvaluationContextProvider contextProvider = new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(
Collections.singletonList(ReactiveSpelExtension.INSTANCE));
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter);
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod(contextProvider, "withReactiveSpelExtensions");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{lastname: true}", "{project: true}");
assertThat(query.getQueryObject().toJson()).isEqualTo(reference.getQueryObject().toJson());
}
private ReactiveStringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
return createQueryForMethod(ReactiveQueryMethodEvaluationContextProvider.DEFAULT, name, parameters);
}
private ReactiveStringBasedMongoQuery createQueryForMethod(
ReactiveQueryMethodEvaluationContextProvider contextProvider, String name, Class<?>... parameters)
throws Exception {
Method method = SampleRepository.class.getMethod(name, parameters);
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method,
new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext());
return new ReactiveStringBasedMongoQuery(queryMethod, operations, PARSER,
QueryMethodEvaluationContextProvider.DEFAULT);
contextProvider);
}
private interface SampleRepository extends Repository<Person, Long> {
@@ -269,5 +294,42 @@ public class ReactiveStringBasedMongoQueryUnitTests {
@Query(value = "{ 'lastname' : ?0 }", exists = true)
Mono<Boolean> existsByLastname(String lastname);
@Query(value = "{ 'lastname' : ?#{hasRole()} }", fields = "{project: ?#{hasRole()}}")
Mono<Document> withReactiveSpelExtensions();
}
public enum ReactiveSpelExtension implements ReactiveEvaluationContextExtension {
INSTANCE;
@Override
public Mono<? extends EvaluationContextExtension> getExtension() {
return Mono.just(SpelExtension.INSTANCE);
}
@Override
public String getExtensionId() {
return "sample";
}
}
public enum SpelExtension implements EvaluationContextExtension {
INSTANCE;
@Override
public Object getRootObject() {
return this;
}
@Override
public String getExtensionId() {
return "sample";
}
public boolean hasRole() {
return true;
}
}
}

View File

@@ -25,11 +25,13 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.bson.Document;
import org.bson.codecs.DecoderContext;
import org.junit.jupiter.api.Test;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -39,6 +41,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
* Unit tests for {@link ParameterBindingJsonReader}.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
class ParameterBindingJsonReaderUnitTests {
@@ -260,6 +263,29 @@ class ParameterBindingJsonReaderUnitTests {
.isEqualTo(new Document("name", new Document("$in", Collections.singletonList("dalinar,kohlin"))));
}
@Test // DATAMONGO-1894
void discoversNoDependenciesInExpression() {
String json = "{ $and : [?#{ [0] == null ? { '$where' : 'true' } : { 'v1' : { '$in' : {[0]} } } }]}";
Optional<ExpressionDependencies> expressionDependencies = new ParameterBindingDocumentCodec()
.getExpressionDependencies(json, it -> new Object(), new SpelExpressionParser());
assertThat(expressionDependencies).contains(ExpressionDependencies.empty());
}
@Test // DATAMONGO-1894
void discoversCorrectlyDependenciesInExpression() {
String json = "{ hello: ?#{hasRole('foo')} }";
Optional<ExpressionDependencies> expressionDependencies = new ParameterBindingDocumentCodec()
.getExpressionDependencies(json, it -> new Object(), new SpelExpressionParser());
assertThat(expressionDependencies).isNotEmpty();
assertThat(expressionDependencies.get()).hasSize(1);
}
@Test // DATAMONGO-2523
void bindSpelExpressionInArrayCorrectly/* closing bracket must not have leading whitespace! */() {