Add support for Value Expressions in Repository Query methods.

Closes #4677
Original pull request: #4683
This commit is contained in:
Marcin Grzejszczak
2024-04-03 09:59:41 +02:00
committed by Mark Paluch
parent db44512c13
commit 8b9182e5a8
25 changed files with 681 additions and 250 deletions

View File

@@ -21,7 +21,12 @@ import java.util.List;
import org.bson.Document;
import org.bson.codecs.configuration.CodecRegistry;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
@@ -43,12 +48,15 @@ import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -71,10 +79,10 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
private final MongoOperations operations;
private final ExecutableFind<?> executableFind;
private final ExecutableUpdate<?> executableUpdate;
private final ExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final Lazy<ParameterBindingDocumentCodec> codec = Lazy
.of(() -> new ParameterBindingDocumentCodec(getCodecRegistry()));
private final ValueExpressionDelegate valueExpressionDelegate;
private final ValueEvaluationContextProvider valueEvaluationContextProvider;
/**
* Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
@@ -83,7 +91,9 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
* @param operations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated use the constructor version with {@link ValueExpressionDelegate}
*/
@Deprecated(since = "4.4.0")
public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, ExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
@@ -100,10 +110,36 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
this.executableFind = operations.query(type);
this.executableUpdate = operations.update(type);
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.valueExpressionDelegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser));
this.valueEvaluationContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters());
}
/**
* Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
*
* @param method must not be {@literal null}.
* @param operations must not be {@literal null}.
* @param delegate must not be {@literal null}
* @since 4.4.0
*/
public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, ValueExpressionDelegate delegate) {
Assert.notNull(operations, "MongoOperations must not be null");
Assert.notNull(method, "MongoQueryMethod must not be null");
this.method = method;
this.operations = operations;
MongoEntityMetadata<?> metadata = method.getEntityInformation();
Class<?> type = metadata.getCollectionEntity().getType();
this.executableFind = operations.query(type);
this.executableUpdate = operations.update(type);
this.valueExpressionDelegate = delegate;
this.valueEvaluationContextProvider = delegate.createValueContextProvider(method.getParameters());
}
@Override
public MongoQueryMethod getQueryMethod() {
return method;
}
@@ -243,7 +279,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor accessor) {
return QueryUtils.applyCollation(query, method.hasAnnotatedCollation() ? method.getAnnotatedCollation() : null,
accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider);
accessor, getExpressionEvaluatorFor(accessor));
}
/**
@@ -346,10 +382,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
*/
protected ParameterBindingContext prepareBindingContext(String source, ConvertingParameterAccessor accessor) {
ExpressionDependencies dependencies = getParameterBindingCodec().captureExpressionDependencies(source,
accessor::getBindableValue, expressionParser);
SpELExpressionEvaluator evaluator = getSpELExpressionEvaluatorFor(dependencies, accessor);
ValueExpressionEvaluator evaluator = getExpressionEvaluatorFor(accessor);
return new ParameterBindingContext(accessor::getBindableValue, evaluator);
}
@@ -374,8 +407,19 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
protected SpELExpressionEvaluator getSpELExpressionEvaluatorFor(ExpressionDependencies dependencies,
ConvertingParameterAccessor accessor) {
return new DefaultSpELExpressionEvaluator(expressionParser, evaluationContextProvider
.getEvaluationContext(getQueryMethod().getParameters(), accessor.getValues(), dependencies));
return new DefaultSpELExpressionEvaluator(new SpelExpressionParser(), valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), dependencies).getEvaluationContext());
}
/**
* Obtain a {@link ValueExpressionEvaluator} suitable to evaluate expressions.
*
* @param accessor must not be {@literal null}.
* @return the {@link ValueExpressionEvaluator}.
* @since 4.4.0
*/
protected ValueExpressionEvaluator getExpressionEvaluatorFor(MongoParameterAccessor accessor) {
return new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, (ValueExpression expression) ->
valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), expression.getExpressionDependencies()));
}
/**
@@ -424,4 +468,5 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
* @since 2.0.4
*/
protected abstract boolean isLimiting();
}

View File

@@ -26,8 +26,15 @@ import org.bson.codecs.configuration.CodecRegistry;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ReactiveValueEvaluationContextProvider;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithProjection;
import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery;
@@ -48,18 +55,22 @@ import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecu
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
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.ValueExpressionDelegate;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.mongodb.MongoClientSettings;
import reactor.util.function.Tuple2;
/**
* Base class for reactive {@link RepositoryQuery} implementations for MongoDB.
@@ -76,8 +87,8 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
private final EntityInstantiators instantiators;
private final FindWithProjection<?> findOperationWithProjection;
private final ReactiveUpdate<?> updateOps;
private final ExpressionParser expressionParser;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final ValueExpressionDelegate valueExpressionDelegate;
private final ReactiveValueEvaluationContextProvider valueEvaluationContextProvider;
/**
* Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and
@@ -87,7 +98,9 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* @param operations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated use the constructor version with {@link ValueExpressionDelegate}
*/
@Deprecated(since = "4.4.0")
public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations,
ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
@@ -99,20 +112,57 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
this.method = method;
this.operations = operations;
this.instantiators = new EntityInstantiators();
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.valueExpressionDelegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser));
MongoEntityMetadata<?> metadata = method.getEntityInformation();
Class<?> type = metadata.getCollectionEntity().getType();
this.findOperationWithProjection = operations.query(type);
this.updateOps = operations.update(type);
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate.createValueContextProvider(
method.getParameters());
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, "ValueEvaluationContextProvider must be reactive");
this.valueEvaluationContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider;
}
/**
* Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and
* {@link MongoOperations}.
*
* @param method must not be {@literal null}.
* @param operations must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @since 4.4.0
*/
public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations,
ValueExpressionDelegate delegate) {
Assert.notNull(method, "MongoQueryMethod must not be null");
Assert.notNull(operations, "ReactiveMongoOperations must not be null");
Assert.notNull(delegate, "ValueExpressionDelegate must not be null");
this.method = method;
this.operations = operations;
this.instantiators = new EntityInstantiators();
this.valueExpressionDelegate = delegate;
MongoEntityMetadata<?> metadata = method.getEntityInformation();
Class<?> type = metadata.getCollectionEntity().getType();
this.findOperationWithProjection = operations.query(type);
this.updateOps = operations.update(type);
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate.createValueContextProvider(
method.getParameters());
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, "ValueEvaluationContextProvider must be reactive");
this.valueEvaluationContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider;
}
@Override
public MongoQueryMethod getQueryMethod() {
return method;
}
@Override
public Publisher<Object> execute(Object[] parameters) {
return method.hasReactiveWrapperParameter() ? executeDeferred(parameters)
@@ -269,7 +319,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor accessor) {
return QueryUtils.applyCollation(query, method.hasAnnotatedCollation() ? method.getAnnotatedCollation() : null,
accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider);
accessor, getValueExpressionEvaluator(accessor));
}
/**
@@ -381,19 +431,19 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
bsonString -> AbstractReactiveMongoQuery.this.decode(evaluator, bsonString, accessor, codec)));
}
private Mono<SpELExpressionEvaluator> expressionEvaluator(String source, MongoParameterAccessor accessor,
ParameterBindingDocumentCodec codec) {
private Mono<Tuple2<ValueExpressionEvaluator, ParameterBindingDocumentCodec>> expressionEvaluator(String source,
MongoParameterAccessor accessor, ParameterBindingDocumentCodec codec) {
ExpressionDependencies dependencies = codec.captureExpressionDependencies(source, accessor::getBindableValue,
expressionParser);
return getSpelEvaluatorFor(dependencies, accessor);
valueExpressionDelegate.getValueExpressionParser());
return getValueExpressionEvaluatorLater(dependencies, accessor).zipWith(Mono.just(codec));
}
private Document decode(SpELExpressionEvaluator expressionEvaluator, String source, MongoParameterAccessor accessor,
private Document decode(Tuple2<ValueExpressionEvaluator, ParameterBindingDocumentCodec> expressionEvaluator, String source, MongoParameterAccessor accessor,
ParameterBindingDocumentCodec codec) {
ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue,
expressionEvaluator);
expressionEvaluator.getT1());
return codec.decode(source, bindingContext);
}
@@ -415,17 +465,54 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* @param accessor must not be {@literal null}.
* @return a {@link Mono} emitting the {@link SpELExpressionEvaluator} when ready.
* @since 3.4
* @deprecated since 4.4.0, use
* {@link #getValueExpressionEvaluatorLater(ExpressionDependencies, MongoParameterAccessor)} instead
*/
@Deprecated(since = "4.4.0")
protected Mono<SpELExpressionEvaluator> getSpelEvaluatorFor(ExpressionDependencies dependencies,
MongoParameterAccessor accessor) {
return evaluationContextProvider
.getEvaluationContextLater(getQueryMethod().getParameters(), accessor.getValues(), dependencies)
.map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(expressionParser,
evaluationContext))
return valueEvaluationContextProvider.getEvaluationContextLater(accessor.getValues(), dependencies)
.map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(
new SpelExpressionParser(), evaluationContext.getEvaluationContext()))
.defaultIfEmpty(DefaultSpELExpressionEvaluator.unsupported());
}
/**
* Obtain a {@link ValueExpressionEvaluator} suitable to evaluate expressions.
*
* @param accessor must not be {@literal null}.
* @since 4.3
*/
ValueExpressionEvaluator getValueExpressionEvaluator(MongoParameterAccessor accessor) {
return new ValueExpressionEvaluator() {
@Override
public <T> T evaluate(String expressionString) {
ValueExpression expression = valueExpressionDelegate.parse(expressionString);
ValueEvaluationContext evaluationContext = valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(),
expression.getExpressionDependencies());
return (T) expression.evaluate(evaluationContext);
}
};
}
/**
* Obtain a {@link Mono publisher} emitting the {@link ValueExpressionEvaluator} suitable to evaluate expressions
* backed by the given dependencies.
*
* @param dependencies must not be {@literal null}.
* @param accessor must not be {@literal null}.
* @return a {@link Mono} emitting the {@link ValueExpressionEvaluator} when ready.
* @since 4.3
*/
protected Mono<ValueExpressionEvaluator> getValueExpressionEvaluatorLater(ExpressionDependencies dependencies,
MongoParameterAccessor accessor) {
return valueEvaluationContextProvider.getEvaluationContextLater(accessor.getValues(), dependencies)
.map(evaluationContext -> new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, valueExpression -> evaluationContext));
}
/**
* @return a {@link Mono} emitting the {@link CodecRegistry} when ready.
* @since 2.4

View File

@@ -21,8 +21,10 @@ import java.util.function.IntUnaryOperator;
import java.util.function.LongUnaryOperator;
import org.bson.Document;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
@@ -31,8 +33,6 @@ import org.springframework.data.mongodb.core.mapping.FieldName;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Meta;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.ExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@@ -62,15 +62,11 @@ abstract class AggregationUtils {
* @param accessor must not be {@literal null}.
* @return the {@link Query} having proper {@link Collation}.
* @see AggregationOptions#getCollation()
* @see CollationUtils#computeCollation(String, ConvertingParameterAccessor, MongoParameters, ExpressionParser,
* QueryMethodEvaluationContextProvider)
*/
static AggregationOptions.Builder applyCollation(AggregationOptions.Builder builder,
@Nullable String collationExpression, ConvertingParameterAccessor accessor, MongoParameters parameters,
ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
@Nullable String collationExpression, ConvertingParameterAccessor accessor, ValueExpressionEvaluator evaluator) {
Collation collation = CollationUtils.computeCollation(collationExpression, accessor, parameters, expressionParser,
evaluationContextProvider);
Collation collation = CollationUtils.computeCollation(collationExpression, accessor, evaluator);
return collation == null ? builder : builder.collation(collation);
}

View File

@@ -20,11 +20,11 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bson.Document;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mongodb.core.query.Collation;
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.expression.ExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
@@ -51,16 +51,13 @@ abstract class CollationUtils {
*
* @param collationExpression
* @param accessor
* @param parameters
* @param expressionParser
* @param evaluationContextProvider
* @param expressionEvaluator
* @return can be {@literal null} if neither {@link ConvertingParameterAccessor#getCollation()} nor
* {@literal collationExpression} are present.
*/
@Nullable
static Collation computeCollation(@Nullable String collationExpression, ConvertingParameterAccessor accessor,
MongoParameters parameters, ExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueExpressionEvaluator expressionEvaluator) {
if (accessor.getCollation() != null) {
return accessor.getCollation();
@@ -73,8 +70,7 @@ abstract class CollationUtils {
if (collationExpression.stripLeading().startsWith("{")) {
ParameterBindingContext bindingContext = ParameterBindingContext.forExpressions(accessor::getBindableValue,
expressionParser, dependencies -> evaluationContextProvider.getEvaluationContext(parameters,
accessor.getValues(), dependencies));
expressionEvaluator);
return Collation.from(CODEC.decode(collationExpression, bindingContext));
}

View File

@@ -18,6 +18,8 @@ package org.springframework.data.mongodb.repository.query;
import org.bson.Document;
import org.bson.json.JsonParseException;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
@@ -28,9 +30,11 @@ 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.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.expression.ExpressionParser;
import org.springframework.util.StringUtils;
@@ -57,11 +61,31 @@ public class PartTreeMongoQuery extends AbstractMongoQuery {
* @param mongoOperations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated since 4.4, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead.
*/
@Deprecated(since = "4.4.0")
public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, ExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
this.processor = method.getResultProcessor();
this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());
this.isGeoNearQuery = method.isGeoNearQuery();
this.context = mongoOperations.getConverter().getMappingContext();
}
/**
* Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}.
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @since 4.4.0
*/
public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations,
ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueExpressionDelegate delegate) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
super(method, mongoOperations, delegate);
this.processor = method.getResultProcessor();
this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());

View File

@@ -22,11 +22,11 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.Document;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.ExpressionParser;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
@@ -73,16 +73,15 @@ class QueryUtils {
* @param query must not be {@literal null}.
* @param collationExpression must not be {@literal null}.
* @param accessor must not be {@literal null}.
* @param expressionEvaluator must not be {@literal null}.
* @return the {@link Query} having proper {@link Collation}.
* @see Query#collation(Collation)
* @since 2.2
*/
static Query applyCollation(Query query, @Nullable String collationExpression, ConvertingParameterAccessor accessor,
MongoParameters parameters, ExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueExpressionEvaluator expressionEvaluator) {
Collation collation = CollationUtils.computeCollation(collationExpression, accessor, parameters, expressionParser,
evaluationContextProvider);
Collation collation = CollationUtils.computeCollation(collationExpression, accessor, expressionEvaluator);
return collation == null ? query : query.collation(collation);
}

View File

@@ -19,6 +19,7 @@ import reactor.core.publisher.Mono;
import org.bson.Document;
import org.bson.json.JsonParseException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -27,10 +28,12 @@ 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.QueryMethodValueEvaluationContextAccessor;
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;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.expression.ExpressionParser;
import org.springframework.util.StringUtils;
@@ -56,10 +59,11 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
* @param mongoOperations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated since 4.4.0, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead.
*/
@Deprecated(since = "4.4.0")
public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
this.processor = method.getResultProcessor();
@@ -68,6 +72,25 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
this.context = mongoOperations.getConverter().getMappingContext();
}
/**
* Creates a new {@link ReactivePartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}.
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @since 4.4.0
*/
public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
ValueExpressionDelegate delegate) {
super(method, mongoOperations, delegate);
this.processor = method.getResultProcessor();
this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());
this.isGeoNearQuery = method.isGeoNearQuery();
this.context = mongoOperations.getConverter().getMappingContext();
}
/**
* Return the {@link PartTree} backing the query.
*

View File

@@ -22,6 +22,7 @@ import java.util.List;
import org.bson.Document;
import org.reactivestreams.Publisher;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
@@ -33,6 +34,7 @@ import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.ExpressionParser;
@@ -48,8 +50,6 @@ import org.springframework.util.ClassUtils;
*/
public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
private final ExpressionParser expressionParser;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final ReactiveMongoOperations reactiveMongoOperations;
private final MongoConverter mongoConverter;
@@ -58,7 +58,9 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
* @param reactiveMongoOperations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead.
*/
@Deprecated(since = "4.4.0")
public ReactiveStringBasedAggregation(ReactiveMongoQueryMethod method,
ReactiveMongoOperations reactiveMongoOperations, ExpressionParser expressionParser,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
@@ -67,8 +69,21 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
this.reactiveMongoOperations = reactiveMongoOperations;
this.mongoConverter = reactiveMongoOperations.getConverter();
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
}
/**
* @param method must not be {@literal null}.
* @param reactiveMongoOperations must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @since 4.4.0
*/
public ReactiveStringBasedAggregation(ReactiveMongoQueryMethod method,
ReactiveMongoOperations reactiveMongoOperations, ValueExpressionDelegate delegate) {
super(method, reactiveMongoOperations, delegate);
this.reactiveMongoOperations = reactiveMongoOperations;
this.mongoConverter = reactiveMongoOperations.getConverter();
}
@Override
@@ -128,8 +143,8 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
AggregationOptions.Builder builder = Aggregation.newAggregationOptions();
AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor, method.getParameters(),
expressionParser, evaluationContextProvider);
AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor,
getValueExpressionEvaluator(accessor));
AggregationUtils.applyMeta(builder, method);
AggregationUtils.applyHint(builder, method);
AggregationUtils.applyReadPreference(builder, method);

View File

@@ -21,6 +21,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.Document;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
@@ -29,9 +30,11 @@ import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
/**
@@ -49,7 +52,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
private final String query;
private final String fieldSpec;
private final ExpressionParser expressionParser;
private final ValueExpressionParser expressionParser;
private final boolean isCountQuery;
private final boolean isExistsQuery;
@@ -63,7 +66,9 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
* @param mongoOperations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead.
*/
@Deprecated(since = "4.4.0")
public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
this(method.getAnnotatedQuery(), method, mongoOperations, expressionParser, evaluationContextProvider);
@@ -78,18 +83,73 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead.
*/
@Deprecated(since = "4.4.0")
public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method,
ReactiveMongoOperations mongoOperations, ExpressionParser expressionParser,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
Assert.notNull(query, "Query must not be null");
Assert.notNull(expressionParser, "SpelExpressionParser must not be null");
this.query = query;
this.expressionParser = expressionParser;
this.expressionParser = ValueExpressionParser.create(() -> expressionParser);
this.fieldSpec = method.getFieldSpecification();
if (method.hasAnnotatedQuery()) {
org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation();
this.isCountQuery = queryAnnotation.count();
this.isExistsQuery = queryAnnotation.exists();
this.isDeleteQuery = queryAnnotation.delete();
if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) {
throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method));
}
} else {
this.isCountQuery = false;
this.isExistsQuery = false;
this.isDeleteQuery = false;
}
}
/**
* Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod},
* {@link MongoOperations} and {@link ValueExpressionDelegate}.
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @since 4.4.0
*/
public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
ValueExpressionDelegate delegate) {
this(method.getAnnotatedQuery(), method, mongoOperations, delegate);
}
/**
* Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link String}, {@link MongoQueryMethod},
* {@link MongoOperations}, {@link ValueExpressionDelegate}.
*
* @param query must not be {@literal null}.
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @since 4.4.0
*/
public ReactiveStringBasedMongoQuery(@NonNull String query, ReactiveMongoQueryMethod method,
ReactiveMongoOperations mongoOperations, ValueExpressionDelegate delegate) {
super(method, mongoOperations, delegate);
Assert.notNull(query, "Query must not be null");
this.query = query;
this.expressionParser = delegate.getValueExpressionParser();
this.fieldSpec = method.getFieldSpecification();
if (method.hasAnnotatedQuery()) {
@@ -141,7 +201,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue,
expressionParser);
return getSpelEvaluatorFor(dependencies, accessor)
return getValueExpressionEvaluatorLater(dependencies, accessor)
.map(it -> new ParameterBindingContext(accessor::getBindableValue, it));
}

View File

@@ -21,6 +21,7 @@ import java.util.function.LongUnaryOperator;
import java.util.stream.Stream;
import org.bson.Document;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
@@ -34,7 +35,9 @@ 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.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.expression.ExpressionParser;
import org.springframework.lang.Nullable;
@@ -53,8 +56,6 @@ public class StringBasedAggregation extends AbstractMongoQuery {
private final MongoOperations mongoOperations;
private final MongoConverter mongoConverter;
private final ExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link StringBasedAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
@@ -63,7 +64,9 @@ public class StringBasedAggregation extends AbstractMongoQuery {
* @param mongoOperations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated since 4.4.0, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead.
*/
@Deprecated(since = "4.4.0")
public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOperations,
ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
@@ -76,8 +79,28 @@ public class StringBasedAggregation extends AbstractMongoQuery {
this.mongoOperations = mongoOperations;
this.mongoConverter = mongoOperations.getConverter();
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
}
/**
* Creates a new {@link StringBasedAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @since 4.4.0
*/
public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOperations,
ValueExpressionDelegate delegate) {
super(method, mongoOperations, delegate);
if (method.isPageQuery()) {
throw new InvalidMongoDbApiUsageException(String.format(
"Repository aggregation method '%s' does not support '%s' return type; Please use 'Slice' or 'List' instead",
method.getName(), method.getReturnType().getType().getSimpleName()));
}
this.mongoOperations = mongoOperations;
this.mongoConverter = mongoOperations.getConverter();
}
@Override
@@ -177,8 +200,8 @@ public class StringBasedAggregation extends AbstractMongoQuery {
AggregationOptions.Builder builder = Aggregation.newAggregationOptions();
AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor, method.getParameters(),
expressionParser, evaluationContextProvider);
AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor,
getExpressionEvaluatorFor(accessor));
AggregationUtils.applyMeta(builder, method);
AggregationUtils.applyHint(builder, method);
AggregationUtils.applyReadPreference(builder, method);

View File

@@ -23,6 +23,8 @@ 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.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
@@ -55,28 +57,69 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
* @param mongoOperations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead.
*/
@Deprecated(since = "4.4.0")
public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations,
ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(method.getAnnotatedQuery(), method, mongoOperations, expressionParser, evaluationContextProvider);
super(method, mongoOperations, expressionParser, evaluationContextProvider);
String query = method.getAnnotatedQuery();
Assert.notNull(query, "Query must not be null");
this.query = query;
this.fieldSpec = method.getFieldSpecification();
if (method.hasAnnotatedQuery()) {
org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation();
this.isCountQuery = queryAnnotation.count();
this.isExistsQuery = queryAnnotation.exists();
this.isDeleteQuery = queryAnnotation.delete();
if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) {
throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method));
}
} else {
this.isCountQuery = false;
this.isExistsQuery = false;
this.isDeleteQuery = false;
}
}
/**
* Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations},
* {@link ValueExpressionDelegate}.
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param expressionSupport must not be {@literal null}.
* @since 4.4.0
*/
public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations,
ValueExpressionDelegate expressionSupport) {
this(method.getAnnotatedQuery(), method, mongoOperations, expressionSupport);
}
/**
* Creates a new {@link StringBasedMongoQuery} for the given {@link String}, {@link MongoQueryMethod},
* {@link MongoOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}.
* {@link MongoOperations}, {@link ValueExpressionDelegate}, {@link QueryMethodValueEvaluationContextAccessor}.
*
* @param query must not be {@literal null}.
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param expressionSupport must not be {@literal null}.
* @since 4.3
*/
public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations,
ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueExpressionDelegate expressionSupport) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
super(method, mongoOperations, expressionSupport);
Assert.notNull(query, "Query must not be null");
Assert.notNull(expressionParser, "SpelExpressionParser must not be null");
this.query = query;
this.fieldSpec = method.getFieldSpecification();

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2024 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 java.util.function.Function;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.repository.query.ValueExpressionDelegate;
class ValueExpressionDelegateValueExpressionEvaluator implements ValueExpressionEvaluator {
private final ValueExpressionDelegate delegate;
private final Function<ValueExpression, ValueEvaluationContext> expressionToContext;
ValueExpressionDelegateValueExpressionEvaluator(ValueExpressionDelegate delegate, Function<ValueExpression, ValueEvaluationContext> expressionToContext) {
this.delegate = delegate;
this.expressionToContext = expressionToContext;
}
@SuppressWarnings("unchecked")
@Override
public <T> T evaluate(String expressionString) {
ValueExpression expression = delegate.parse(expressionString);
return (T) expression.evaluate(expressionToContext.apply(expression));
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright 2020-2024 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.support;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
/**
* Caching variant of {@link ExpressionParser}. This implementation does not support
* {@link #parseExpression(String, ParserContext) parsing with ParseContext}.
*
* @author Mark Paluch
* @since 3.1
*/
class CachingExpressionParser implements ExpressionParser {
private final ExpressionParser delegate;
private final Map<String, Expression> cache = new ConcurrentHashMap<>();
CachingExpressionParser(ExpressionParser delegate) {
this.delegate = delegate;
}
@Override
public Expression parseExpression(String expressionString) throws ParseException {
return cache.computeIfAbsent(expressionString, delegate::parseExpression);
}
@Override
public Expression parseExpression(String expressionString, ParserContext context) throws ParseException {
throw new UnsupportedOperationException("Parsing using ParserContext is not supported");
}
}

View File

@@ -42,10 +42,9 @@ import org.springframework.data.repository.core.support.RepositoryComposition.Re
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
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.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -59,11 +58,10 @@ import org.springframework.util.Assert;
*/
public class MongoRepositoryFactory extends RepositoryFactorySupport {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
private final MongoOperations operations;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
@Nullable private QueryMethodValueEvaluationContextAccessor accessor;
/**
* Creates a new {@link MongoRepositoryFactory} with the given {@link MongoOperations}.
@@ -148,8 +146,8 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new MongoQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate));
}
public <T, ID> MongoEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
@@ -170,21 +168,9 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
* @author Oliver Gierke
* @author Thomas Darimont
*/
private static class MongoQueryLookupStrategy implements QueryLookupStrategy {
private final MongoOperations operations;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER);
public MongoQueryLookupStrategy(MongoOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.operations = operations;
this.evaluationContextProvider = evaluationContextProvider;
this.mappingContext = mappingContext;
}
private record MongoQueryLookupStrategy(MongoOperations operations,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
ValueExpressionDelegate expressionSupport) implements QueryLookupStrategy {
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
@@ -197,14 +183,13 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new StringBasedMongoQuery(namedQuery, queryMethod, operations, expressionParser,
evaluationContextProvider);
return new StringBasedMongoQuery(namedQuery, queryMethod, operations, expressionSupport);
} else if (queryMethod.hasAnnotatedAggregation()) {
return new StringBasedAggregation(queryMethod, operations, expressionParser, evaluationContextProvider);
return new StringBasedAggregation(queryMethod, operations, expressionSupport);
} else if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedMongoQuery(queryMethod, operations, expressionParser, evaluationContextProvider);
return new StringBasedMongoQuery(queryMethod, operations, expressionSupport);
} else {
return new PartTreeMongoQuery(queryMethod, operations, expressionParser, evaluationContextProvider);
return new PartTreeMongoQuery(queryMethod, operations, expressionSupport);
}
}
}

View File

@@ -21,6 +21,7 @@ import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Optional;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -42,11 +43,10 @@ import org.springframework.data.repository.core.support.RepositoryComposition.Re
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.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -60,11 +60,10 @@ import org.springframework.util.Assert;
*/
public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
private final ReactiveMongoOperations operations;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
@Nullable private QueryMethodValueEvaluationContextAccessor accessor;
/**
* Creates a new {@link ReactiveMongoRepositoryFactory} with the given {@link ReactiveMongoOperations}.
@@ -133,13 +132,12 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
return targetRepository;
}
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new MongoQueryLookupStrategy(operations,
(ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider, mappingContext));
@Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate));
}
@Override
public <T, ID> MongoEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return getEntityInformation(domainClass, null);
}
@@ -160,21 +158,9 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
* @author Mark Paluch
* @author Christoph Strobl
*/
private static class MongoQueryLookupStrategy implements QueryLookupStrategy {
private final ReactiveMongoOperations operations;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER);
MongoQueryLookupStrategy(ReactiveMongoOperations operations,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.operations = operations;
this.evaluationContextProvider = evaluationContextProvider;
this.mappingContext = mappingContext;
}
private record MongoQueryLookupStrategy(ReactiveMongoOperations operations,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
ValueExpressionDelegate delegate) implements QueryLookupStrategy {
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
@@ -187,14 +173,13 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ReactiveStringBasedMongoQuery(namedQuery, queryMethod, operations, expressionParser,
evaluationContextProvider);
return new ReactiveStringBasedMongoQuery(namedQuery, queryMethod, operations, delegate);
} else if (queryMethod.hasAnnotatedAggregation()) {
return new ReactiveStringBasedAggregation(queryMethod, operations, expressionParser, evaluationContextProvider);
return new ReactiveStringBasedAggregation(queryMethod, operations, delegate);
} else if (queryMethod.hasAnnotatedQuery()) {
return new ReactiveStringBasedMongoQuery(queryMethod, operations, expressionParser, evaluationContextProvider);
return new ReactiveStringBasedMongoQuery(queryMethod, operations, delegate);
} else {
return new ReactivePartTreeMongoQuery(queryMethod, operations, expressionParser, evaluationContextProvider);
return new ReactivePartTreeMongoQuery(queryMethod, operations, delegate);
}
}
}

View File

@@ -222,7 +222,7 @@ class JsonScanner {
while (c == '$' || c == '_' || Character.isLetterOrDigit(c) || c == '#' || c == '{' || c == '['
|| (isExpression && isExpressionAllowedChar(c))) {
if (charCount == 0 && c == '#') {
if (charCount == 0 && (c == '#' || c == '$')) {
isExpression = true;
} else if (isExpression) {
if (c == '{') {

View File

@@ -26,6 +26,8 @@ import org.springframework.data.util.Lazy;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
@@ -46,14 +48,25 @@ public class ParameterBindingContext {
* @param valueProvider
* @param expressionParser
* @param evaluationContext
* @deprecated since 4.3, use {@link #ParameterBindingContext(ValueProvider, ExpressionParser, Supplier)} instead.
* @deprecated since 4.4.0, use {@link #ParameterBindingContext(ValueProvider, ExpressionParser, Supplier)} instead.
*/
@Deprecated(since = "4.3")
@Deprecated(since = "4.4.0")
public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser expressionParser,
EvaluationContext evaluationContext) {
this(valueProvider, expressionParser, () -> evaluationContext);
}
/**
* @param valueProvider
* @param expressionEvaluator
* @since 3.1
* @deprecated since 4.4.0, use {@link #ParameterBindingContext(ValueProvider, ValueExpressionEvaluator)} instead.
*/
@Deprecated(since = "4.4.0")
public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvaluator expressionEvaluator) {
this(valueProvider, (ValueExpressionEvaluator) expressionEvaluator);
}
/**
* @param valueProvider
* @param expressionParser
@@ -62,7 +75,7 @@ public class ParameterBindingContext {
*/
public ParameterBindingContext(ValueProvider valueProvider, ExpressionParser expressionParser,
Supplier<EvaluationContext> evaluationContext) {
this(valueProvider, new EvaluationContextExpressionEvaluator(valueProvider, expressionParser) {
this(valueProvider, new EvaluationContextExpressionEvaluator(valueProvider, unwrap(expressionParser)) {
@Override
public EvaluationContext getEvaluationContext(String expressionString) {
return evaluationContext.get();
@@ -70,21 +83,30 @@ public class ParameterBindingContext {
});
}
/**
* @param valueProvider
* @param expressionEvaluator
* @since 3.1
* @deprecated since 4.3, use {@link #ParameterBindingContext(ValueProvider, ValueExpressionEvaluator)} instead.
*/
@Deprecated(since = "4.3")
public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvaluator expressionEvaluator) {
this(valueProvider, (ValueExpressionEvaluator) expressionEvaluator);
private static ExpressionParser unwrap(ExpressionParser expressionParser) {
return new ExpressionParser() {
@Override
public Expression parseExpression(String expressionString) throws ParseException {
return expressionParser.parseExpression(unwrap(expressionString));
}
@Override
public Expression parseExpression(String expressionString, ParserContext context) throws ParseException {
return expressionParser.parseExpression(unwrap(expressionString), context);
}
};
}
private static String unwrap(String expressionString) {
return expressionString.startsWith("#{") && expressionString.endsWith("}")
? expressionString.substring(2, expressionString.length() - 1).trim()
: expressionString;
}
/**
* @param valueProvider
* @param expressionEvaluator
* @since 4.3
* @since 4.4.0
*/
public ParameterBindingContext(ValueProvider valueProvider, ValueExpressionEvaluator expressionEvaluator) {
this.valueProvider = valueProvider;
@@ -117,6 +139,20 @@ public class ParameterBindingContext {
});
}
/**
* Create a new {@link ParameterBindingContext} that is capable of expression parsing.
*
* @param valueProvider
* @param expressionEvaluator
* @return
* @since 4.4.0
*/
public static ParameterBindingContext forExpressions(ValueProvider valueProvider,
ValueExpressionEvaluator expressionEvaluator) {
return new ParameterBindingContext(valueProvider, expressionEvaluator);
}
@Nullable
public Object bindableValueForIndex(int index) {
return valueProvider.getBindableValue(index);

View File

@@ -41,11 +41,11 @@ import org.bson.codecs.*;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.json.JsonParseException;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mongodb.core.mapping.FieldName;
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;
@@ -194,7 +194,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
* @since 3.1
*/
public ExpressionDependencies captureExpressionDependencies(@Nullable String json, ValueProvider valueProvider,
ExpressionParser expressionParser) {
ValueExpressionParser expressionParser) {
if (!StringUtils.hasText(json)) {
return ExpressionDependencies.none();
@@ -389,10 +389,10 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
private static final Object PLACEHOLDER = new Object();
private final ExpressionParser expressionParser;
private final ValueExpressionParser expressionParser;
private final List<ExpressionDependencies> dependencies = new ArrayList<>();
DependencyCapturingExpressionEvaluator(ExpressionParser expressionParser) {
DependencyCapturingExpressionEvaluator(ValueExpressionParser expressionParser) {
this.expressionParser = expressionParser;
}
@@ -400,7 +400,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
@Override
public <T> T evaluate(String expression) {
dependencies.add(ExpressionDependencies.discover(expressionParser.parseExpression(expression)));
dependencies.add(expressionParser.parse(expression).getExpressionDependencies());
return (T) PLACEHOLDER;
}

View File

@@ -64,9 +64,9 @@ import org.springframework.util.ObjectUtils;
*/
public class ParameterBindingJsonReader extends AbstractBsonReader {
private static final Pattern ENTIRE_QUERY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$|^[\\?:]#\\{.*\\}$");
private static final Pattern ENTIRE_QUERY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$|^[\\?:][#$]\\{.*\\}$");
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[\\?:]#\\{.*\\}");
private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[\\?:][#$]\\{.*\\}");
private static final Pattern SPEL_PARAMETER_BINDING_PATTERN = Pattern.compile("('\\?(\\d+)'|\\?(\\d+))");
private final ParameterBindingContext bindingContext;
@@ -374,6 +374,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
String binding = regexMatcher.group();
String expression = binding.substring(3, binding.length() - 1);
String expressionString = binding.substring(1);
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression); // ?0 '?0'
Map<String, Object> innerSpelVariables = new HashMap<>();
@@ -385,6 +386,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
Object value = getBindableValueForIndex(index);
String varName = "__QVar" + innerSpelVariables.size();
expression = expression.replace(group, "#" + varName);
expressionString = expressionString.replace(group, "#" + varName);
if(group.startsWith("'")) { // retain the string semantic
innerSpelVariables.put(varName, nullSafeToString(value));
} else {
@@ -392,7 +394,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
}
}
Object value = evaluateExpression(expression, innerSpelVariables);
Object value = evaluateExpression(expressionString, innerSpelVariables);
bindableValue.setValue(value);
bindableValue.setType(bsonTypeForValue(value));
return bindableValue;
@@ -421,6 +423,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
String binding = regexMatcher.group();
String expression = binding.substring(3, binding.length() - 1);
String expressionString = binding.substring(1);
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression);
Map<String, Object> innerSpelVariables = new HashMap<>();
@@ -432,6 +435,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
Object value = getBindableValueForIndex(index);
String varName = "__QVar" + innerSpelVariables.size();
expression = expression.replace(group, "#" + varName);
expressionString = expressionString.replace(group, "#" + varName);
if(group.startsWith("'")) { // retain the string semantic
innerSpelVariables.put(varName, nullSafeToString(value));
} else {
@@ -439,7 +443,8 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
}
}
computedValue = computedValue.replace(binding, nullSafeToString(evaluateExpression(expression, innerSpelVariables)));
computedValue = computedValue.replace(binding,
nullSafeToString(evaluateExpression(expressionString, innerSpelVariables)));
bindableValue.setValue(computedValue);
bindableValue.setType(BsonType.STRING);
@@ -1081,7 +1086,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
}
verifyToken(JsonTokenType.RIGHT_PAREN);
String dateTimeString = token.getValue(String.class);
try {

View File

@@ -34,12 +34,14 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.util.TypeInformation;
import org.springframework.mock.env.MockEnvironment;
/**
* Unit tests for {@link BasicMongoPersistentEntity}.
@@ -87,6 +89,19 @@ public class BasicMongoPersistentEntityUnitTests {
assertThat(entity.getCollection()).isEqualTo("otherReference");
}
@Test // GH-2764
void collectionAllowsReferencingProperties() {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("collectionName", "reference");
BasicMongoPersistentEntity<DynamicallyMappedUsingPropertyPlaceholder> entity = new BasicMongoPersistentEntity<>(
TypeInformation.of(DynamicallyMappedUsingPropertyPlaceholder.class));
entity.setEnvironment(environment);
assertThat(entity.getCollection()).isEqualTo("reference_cat");
}
@Test // DATAMONGO-937
void shouldDetectLanguageCorrectly() {
@@ -325,6 +340,9 @@ public class BasicMongoPersistentEntityUnitTests {
@Document("#{@myBean.collectionName}")
class DynamicallyMapped {}
@Document("${collectionName}_cat")
class DynamicallyMappedUsingPropertyPlaceholder {}
class CollectionProvider {
String collectionName;

View File

@@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -36,6 +37,9 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -43,6 +47,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
@@ -71,7 +76,8 @@ 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.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import com.mongodb.MongoClientSettings;
@@ -566,7 +572,11 @@ class AbstractMongoQueryUnitTests {
private boolean isLimitingQuery;
MongoQueryFake(MongoQueryMethod method, MongoOperations operations) {
super(method, operations, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT);
super(method, operations,
new ValueExpressionDelegate(
new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(),
Collections.emptySet()),
ValueExpressionParser.create(SpelExpressionParser::new)));
}
@Override

View File

@@ -22,6 +22,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -36,6 +37,10 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mongodb.core.Person;
import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ReactiveFindOperation.ReactiveFind;
@@ -58,7 +63,8 @@ import org.springframework.data.mongodb.repository.Update;
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.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import com.mongodb.MongoClientSettings;
@@ -197,7 +203,7 @@ class AbstractReactiveMongoQueryUnitTests {
createQueryForMethod("findWithCollationUsingPlaceholdersInDocumentByFirstName", String.class, String.class,
int.class) //
.executeBlocking(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());
@@ -299,8 +305,12 @@ class AbstractReactiveMongoQueryUnitTests {
private boolean isLimitingQuery;
ReactiveMongoQueryFake(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations) {
super(method, operations, new SpelExpressionParser(),
ReactiveExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT);
super(method, operations,
new ValueExpressionDelegate(
new QueryMethodValueEvaluationContextAccessor(
new StandardEnvironment(),
Collections.emptySet()),
ValueExpressionParser.create(SpelExpressionParser::new)));
}
@Override

View File

@@ -19,14 +19,13 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.bson.Document;
@@ -35,7 +34,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mongodb.core.ReactiveFindOperation.ReactiveFind;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
@@ -51,8 +55,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.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -67,7 +73,8 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
@MockitoSettings(strictness = Strictness.LENIENT)
public class ReactiveStringBasedMongoQueryUnitTests {
SpelExpressionParser PARSER = new SpelExpressionParser();
ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
StandardEnvironment environment = new StandardEnvironment();
@Mock ReactiveMongoOperations operations;
@Mock DbRefResolver factory;
@@ -75,13 +82,18 @@ public class ReactiveStringBasedMongoQueryUnitTests {
MongoConverter converter;
Map<String, Object> properties = new HashMap<>();
MapPropertySource propertySource = new MapPropertySource("mock", properties);
@BeforeEach
public void setUp() {
when(operations.query(any())).thenReturn(reactiveFind);
when(operations.execute(any())).thenReturn(Flux.empty());
environment.getPropertySources().addFirst(propertySource);
this.converter = new MappingMongoConverter(factory, new MongoMappingContext());
when(operations.query(any())).thenReturn(reactiveFind);
when(operations.execute(any())).thenReturn(Flux.empty());
}
@Test // DATAMONGO-1444
@@ -188,10 +200,23 @@ public class ReactiveStringBasedMongoQueryUnitTests {
assertThat(query.getQueryObject()).isEqualTo(reference.getQueryObject());
}
@Test // GH-3050
public void shouldSupportPropertiesInCustomQueries() throws Exception {
properties.put("foo", "bar");
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter);
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithProperty");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : 'bar'}");
assertThat(query.getQueryObject()).isEqualTo(reference.getQueryObject());
}
@Test // DATAMONGO-1444
public void shouldSupportExpressionsInCustomQueriesWithNestedObject() throws Exception {
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, true, "param1", "param2");
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, true, "param1");
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithExpressionAndNestedObject",
boolean.class, String.class);
@@ -232,11 +257,8 @@ public class ReactiveStringBasedMongoQueryUnitTests {
@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");
ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("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}");
@@ -244,21 +266,17 @@ public class ReactiveStringBasedMongoQueryUnitTests {
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)
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,
contextProvider);
QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(
environment, Collections.singletonList(ReactiveSpelExtension.INSTANCE));
return new ReactiveStringBasedMongoQuery(queryMethod, operations, new ValueExpressionDelegate(accessor, PARSER));
}
private interface SampleRepository extends Repository<Person, Long> {
@@ -290,6 +308,9 @@ public class ReactiveStringBasedMongoQueryUnitTests {
@Query("{'lastname': ?#{[0]} }")
Flux<Person> findByQueryWithExpression(String param0);
@Query("{'lastname': ?${foo} }")
Flux<Person> findByQueryWithProperty();
@Query("{'id':?#{ [0] ? { $exists :true} : [1] }}")
Flux<Person> findByQueryWithExpressionAndNestedObject(boolean param0, String param1);

View File

@@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -39,9 +40,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mongodb.core.DbCallback;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
@@ -59,7 +63,9 @@ 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.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import com.mongodb.MongoClientSettings;
@@ -77,7 +83,8 @@ import com.mongodb.reactivestreams.client.MongoClients;
@MockitoSettings(strictness = Strictness.LENIENT)
public class StringBasedMongoQueryUnitTests {
SpelExpressionParser PARSER = new SpelExpressionParser();
ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
StandardEnvironment environment = new StandardEnvironment();
@Mock MongoOperations operations;
@Mock ExecutableFind<Object> findOperation;
@@ -85,10 +92,14 @@ public class StringBasedMongoQueryUnitTests {
MongoConverter converter;
Map<String, Object> properties = new HashMap<>();
MapPropertySource propertySource = new MapPropertySource("mock", properties);
@BeforeEach
public void setUp() {
this.converter = new MappingMongoConverter(factory, new MongoMappingContext());
environment.getPropertySources().addFirst(propertySource);
doReturn(findOperation).when(operations).query(any());
doReturn(MongoClientSettings.getDefaultCodecRegistry()).when(operations).execute(any());
@@ -293,10 +304,46 @@ public class StringBasedMongoQueryUnitTests {
assertThat(query.getQueryObject()).isEqualTo(reference.getQueryObject());
}
@Test // GH-3050
public void shouldSupportExpressionsAndPropertiesInCustomQueries() {
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "Matthews");
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithExpressionAndProperty", String.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery(
"{'lastname' : 'Matthews', 'firstname' : 'some-default'}");
assertThat(query.getQueryObject()).isEqualTo(reference.getQueryObject());
}
@Test // GH-3050
public void shouldSupportPropertiesInCustomQueries() {
properties.put("foo", "bar");
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter);
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithProperty");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : 'bar'}");
assertThat(query.getQueryObject()).isEqualTo(reference.getQueryObject());
}
@Test // GH-3050
public void shouldFailWhenPropertiesWithNoDefaultValueInCustomQueries() {
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter);
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithProperty");
assertThatThrownBy(() -> mongoQuery.createQuery(accessor))
.isInstanceOf(EvaluationException.class)
.hasMessageContaining("Could not resolve placeholder 'foo' in value \"${foo}\"");
}
@Test // DATAMONGO-1244
public void shouldSupportExpressionsInCustomQueriesWithNestedObject() {
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, true, "param1", "param2");
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, true, "param1");
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithExpressionAndNestedObject", boolean.class,
String.class);
@@ -707,7 +754,9 @@ public class StringBasedMongoQueryUnitTests {
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class),
factory, converter.getMappingContext());
return new StringBasedMongoQuery(queryMethod, operations, PARSER, QueryMethodEvaluationContextProvider.DEFAULT);
QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(
environment, Collections.emptySet());
return new StringBasedMongoQuery(queryMethod, operations, new ValueExpressionDelegate(accessor, PARSER));
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage(), e);
@@ -776,6 +825,12 @@ public class StringBasedMongoQueryUnitTests {
@Query("{'lastname': ?#{[0]} }")
List<Person> findByQueryWithExpression(String param0);
@Query("{'lastname': ?#{[0]}, 'firstname': ?${absent-property:some-default} }")
List<Person> findByQueryWithExpressionAndProperty(String param0);
@Query("{'lastname': ?${foo} }")
List<Person> findByQueryWithProperty();
@Query("{'id':?#{ [0] ? { $exists :true} : [1] }}")
List<Person> findByQueryWithExpressionAndNestedObject(boolean param0, String param1);

View File

@@ -29,6 +29,9 @@ import org.bson.BsonBinarySubType;
import org.bson.Document;
import org.bson.codecs.DecoderContext;
import org.junit.jupiter.api.Test;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
@@ -261,7 +264,7 @@ class ParameterBindingJsonReaderUnitTests {
};
ParameterBindingJsonReader reader = new ParameterBindingJsonReader("{ 'name':'?0' }",
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target).isEqualTo(new Document("name", "value"));
@@ -309,7 +312,8 @@ class ParameterBindingJsonReaderUnitTests {
String json = "{ $and : [?#{ [0] == null ? { '$where' : 'true' } : { 'v1' : { '$in' : {[0]} } } }]}";
ExpressionDependencies expressionDependencies = new ParameterBindingDocumentCodec()
.captureExpressionDependencies(json, it -> new Object(), new SpelExpressionParser());
.captureExpressionDependencies(json, it -> new Object(),
ValueExpressionParser.create(SpelExpressionParser::new));
assertThat(expressionDependencies).isEqualTo(ExpressionDependencies.none());
}
@@ -320,7 +324,8 @@ class ParameterBindingJsonReaderUnitTests {
String json = "{ hello: ?#{hasRole('foo')} }";
ExpressionDependencies expressionDependencies = new ParameterBindingDocumentCodec()
.captureExpressionDependencies(json, it -> new Object(), new SpelExpressionParser());
.captureExpressionDependencies(json, it -> new Object(),
ValueExpressionParser.create(SpelExpressionParser::new));
assertThat(expressionDependencies).isNotEmpty();
assertThat(expressionDependencies.get()).hasSize(1);
@@ -343,7 +348,7 @@ class ParameterBindingJsonReaderUnitTests {
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(
"{ 'isBatman' : ?#{ T(" + this.getClass().getName() + ").isBatman() ? [0] : [1] }}",
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target).isEqualTo(new Document("isBatman", "nooo"));
@@ -358,7 +363,7 @@ class ParameterBindingJsonReaderUnitTests {
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(
"{ 'isBatman' : ?#{ T(" + this.getClass().getName() + ").isBatman() ? '?0' : '?1' }}",
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target).isEqualTo(new Document("isBatman", "nooo"));
@@ -373,7 +378,7 @@ class ParameterBindingJsonReaderUnitTests {
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(
"{ 'isBatman' : \"?#{ T(" + this.getClass().getName() + ").isBatman() ? '?0' : '?1' }\" }",
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target).isEqualTo(new Document("isBatman", "nooo"));
@@ -391,7 +396,7 @@ class ParameterBindingJsonReaderUnitTests {
+ ").isBatman() ? {'_class': { '$eq' : 'region' }} : { '$and' : { {'_class': { '$eq' : 'region' } }, {'user.supervisor': principal.id } } } }";
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target)
@@ -406,7 +411,7 @@ class ParameterBindingJsonReaderUnitTests {
String json = "?#{ true ? { 'name': #name } : { 'name' : #name + 'trouble' } }";
new ParameterBindingDocumentCodec().captureExpressionDependencies(json, (index) -> args[index],
new SpelExpressionParser());
ValueExpressionParser.create(SpelExpressionParser::new));
}
@Test // GH-3871, GH-4089
@@ -418,7 +423,7 @@ class ParameterBindingJsonReaderUnitTests {
.getEvaluationContext(args);
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target).isEqualTo(new Document("name", "expected"));
@@ -434,7 +439,7 @@ class ParameterBindingJsonReaderUnitTests {
assertThatExceptionOfType(ParseException.class).isThrownBy(() -> {
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
});
@@ -449,7 +454,7 @@ class ParameterBindingJsonReaderUnitTests {
.getEvaluationContext(args);
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
@@ -467,7 +472,7 @@ class ParameterBindingJsonReaderUnitTests {
String json = "?#{ T(" + this.getClass().getName() + ").applyFilterByUser('?0' ,principal.id) }";
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target)
@@ -485,7 +490,7 @@ class ParameterBindingJsonReaderUnitTests {
String json = "?0";
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), () -> evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target).isEqualTo(new Document("itWorks", true));