From 66fae82798acd4d989394ffd92c6a991b3a6a9cb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 30 Jun 2020 12:12:44 +0200 Subject: [PATCH] DATAMONGO-1894 - Use reactive SpEL extensions for SpEL evaluation in query execution. Original Pull Request: #874 --- .../query/AbstractReactiveMongoQuery.java | 55 +++++---- .../repository/query/CollationUtils.java | 5 +- .../query/DefaultSpELExpressionEvaluator.java | 47 ++++++++ .../query/NoOpExpressionEvaluator.java | 34 ++++++ .../query/ReactiveMongoQueryExecution.java | 10 +- .../query/ReactivePartTreeMongoQuery.java | 16 +-- .../query/ReactiveStringBasedAggregation.java | 105 ++++++++++++------ .../query/ReactiveStringBasedMongoQuery.java | 57 +++++++--- .../query/StringBasedMongoQuery.java | 29 ++++- .../ReactiveMongoRepositoryFactory.java | 8 +- .../ReactiveMongoRepositoryFactoryBean.java | 24 ++-- .../util/json/ParameterBindingContext.java | 66 +++++++---- .../json/ParameterBindingDocumentCodec.java | 53 ++++++++- .../util/json/ParameterBindingJsonReader.java | 1 + .../ReactiveMongoRepositoryTests.java | 3 +- .../SimpleReactiveMongoRepositoryTests.java | 3 +- .../AbstractReactiveMongoQueryUnitTests.java | 41 ++++--- ...activeStringBasedAggregationUnitTests.java | 6 +- ...eactiveStringBasedMongoQueryUnitTests.java | 84 ++++++++++++-- .../ParameterBindingJsonReaderUnitTests.java | 26 +++++ 20 files changed, 517 insertions(+), 156 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/NoOpExpressionEvaluator.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java index 69c834e4f..0cd2dd4b5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java @@ -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 execute(Object[] parameters) { return method.hasReactiveWrapperParameter() ? executeDeferred(parameters) : execute(new MongoParametersParameterAccessor(method, parameters)); } @SuppressWarnings("unchecked") - private Object executeDeferred(Object[] parameters) { + private Publisher executeDeferred(Object[] parameters) { ReactiveMongoParameterAccessor parameterAccessor = new ReactiveMongoParameterAccessor(method, parameters); - if (getQueryMethod().isCollectionQuery()) { - return Flux.defer(() -> (Publisher) execute(parameterAccessor)); - } - - return Mono.defer(() -> (Mono) execute(parameterAccessor)); + return execute(parameterAccessor); } - private Object execute(MongoParameterAccessor parameterAccessor) { + private Publisher 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 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 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 createQuery(ConvertingParameterAccessor accessor); /** * Returns whether the query should get a count projection applied. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java index c77c37f65..74d9c2153 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java @@ -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)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java new file mode 100644 index 000000000..6dc63be9d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java @@ -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 evaluate(String expression) { + return (T) parser.parseExpression(expression).getValue(context, Object.class); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/NoOpExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/NoOpExpressionEvaluator.java new file mode 100644 index 000000000..416c8fbbf --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/NoOpExpressionEvaluator.java @@ -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 evaluate(String expression) { + throw new UnsupportedOperationException("Expression evaluation not supported"); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java index e2b9d51da..2ca038ba8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java @@ -48,7 +48,7 @@ import org.springframework.util.ClassUtils; */ interface ReactiveMongoQueryExecution { - Object execute(Query query, Class type, String collection); + Publisher 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 execute(Query query, Class type, String collection) { Flux> 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 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 execute(Query query, Class type, String collection) { + return (Publisher) converter.convert(delegate.execute(query, type, collection)); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java index 0bce40c3c..f2226f6b0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java @@ -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 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 createCountQuery(ConvertingParameterAccessor accessor) { + return Mono.fromSupplier(() -> new MongoQueryCreator(tree, accessor, context, false).createQuery()); } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index 347df2fd2..8df312ec3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -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 doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, Class typeToRead) { - Class sourceType = method.getDomainClass(); - Class targetType = typeToRead; + return computePipeline(accessor).flatMapMany(it -> { - List 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 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 computePipeline(ConvertingParameterAccessor accessor) { - return AggregationUtils.computePipeline(getQueryMethod(), accessor, expressionParser, evaluationContextProvider); + private Mono> computePipeline(ConvertingParameterAccessor accessor) { + + MongoQueryMethod method = getQueryMethod(); + + List> stages = new ArrayList<>(method.getAnnotatedAggregation().length); + + for (String source : method.getAnnotatedAggregation()) { + + Optional dependencies = CODEC.getExpressionDependencies(source, + accessor::getBindableValue, expressionParser); + + Mono 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 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 createQuery(ConvertingParameterAccessor accessor) { throw new UnsupportedOperationException("No query support for aggregation"); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java index b84fca368..d8a57f0e8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java @@ -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 createQuery(ConvertingParameterAccessor accessor) { - ParameterBindingContext bindingContext = new ParameterBindingContext((accessor::getBindableValue), expressionParser, - () -> evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), accessor.getValues())); + Mono queryObject = getBindingContext(accessor, expressionParser, this.query) + .map(it -> CODEC.decode(this.query, it)); + Mono 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 getBindingContext(ConvertingParameterAccessor accessor, + ExpressionParser expressionParser, String json) { + + Optional dependencies = CODEC.getExpressionDependencies(json, accessor::getBindableValue, + expressionParser); + + Mono 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)); } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index 1eb4f66c3..c1a0c7343 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -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 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() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index d6508e8a4..6ab3ae0bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -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 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, MongoPersistentProperty> mappingContext; MongoQueryLookupStrategy(ReactiveMongoOperations operations, - QueryMethodEvaluationContextProvider evaluationContextProvider, + ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, MappingContext, MongoPersistentProperty> mappingContext) { this.operations = operations; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java index cdbe54c6c..cc646d9ae 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java @@ -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, 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, S, return factory; } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createDefaultQueryMethodEvaluationContextProvider(ListableBeanFactory) + */ + @Override + protected Optional createDefaultQueryMethodEvaluationContextProvider( + ListableBeanFactory beanFactory) { + return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory)); + } + /** * Creates and initializes a {@link RepositoryFactorySupport} instance. * @@ -114,10 +125,7 @@ public class ReactiveMongoRepositoryFactoryBean, S, /* * (non-Javadoc) - * - * @see - * org.springframework.data.repository.support.RepositoryFactoryBeanSupport - * #afterPropertiesSet() + * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#afterPropertiesSet() */ @Override public void afterPropertiesSet() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java index d2ce70075..519ff1dbd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java @@ -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; + 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) { + this(valueProvider, new SpELExpressionEvaluator() { + @Override + public 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 contextFunction) { + + return new ParameterBindingContext(valueProvider, new SpELExpressionEvaluator() { + @Override + public 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() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java index 33eeee9ba..69a0fec2c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java @@ -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 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 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 getExpressionDependencies(@Nullable String json, ValueProvider valueProvider, + ExpressionParser expressionParser) { + + if (StringUtils.isEmpty(json)) { + return Optional.empty(); + } + + List dependencies = new ArrayList<>(); + + ParameterBindingContext context = new ParameterBindingContext(valueProvider, new SpELExpressionEvaluator() { + @Override + public 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 reader.readEndArray(); return list; } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java index 22821a63d..e0e1a61ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java @@ -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; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java index ef388005e..09700dcfc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java @@ -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; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java index 072e1ba40..a69e2903d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java @@ -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); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java index bed41afb3..23fbf8c13 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java @@ -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 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 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 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 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 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 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 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 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 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 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 createQuery(ConvertingParameterAccessor accessor) { + return Mono.just(new BasicQuery("{'foo':'bar'}")); + } + + Object executeBlocking(Object[] parameters) { + return Flux.from(super.execute(parameters)).collectList().block(); } @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java index f899f3c68..35fb113cb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java @@ -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 aggregationCaptor = ArgumentCaptor.forClass(TypedAggregation.class); ArgumentCaptor 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 pipelineOf(AggregationInvocation invocation) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java index 72eb4548f..19f1ae485 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java @@ -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 { @@ -269,5 +294,42 @@ public class ReactiveStringBasedMongoQueryUnitTests { @Query(value = "{ 'lastname' : ?0 }", exists = true) Mono existsByLastname(String lastname); + + @Query(value = "{ 'lastname' : ?#{hasRole()} }", fields = "{project: ?#{hasRole()}}") + Mono withReactiveSpelExtensions(); + } + + public enum ReactiveSpelExtension implements ReactiveEvaluationContextExtension { + + INSTANCE; + + @Override + public Mono 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; + } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java index 3b1fb761c..bc774538f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java @@ -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 = 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 = 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! */() {