diff --git a/pom.xml b/pom.xml index 3fa54424af..78fb3035d0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-GH-4677-SNAPSHOT pom Spring Data MongoDB @@ -26,7 +26,7 @@ multi spring-data-mongodb - 3.4.0-SNAPSHOT + 3.4.0-GH-3049-SNAPSHOT 5.1.4 ${mongo} 1.19 diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index a3dc49f892..ae7137cdd5 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-GH-4677-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index acdc13437d..227a13850d 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-GH-4677-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index fafe9c8793..801c987059 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-GH-4677-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index 61bfa0f7b3..1c817b17f2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -21,7 +21,12 @@ 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.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 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 AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, E 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 @@ Query applyAnnotatedDefaultSortIfPresent(Query query) { 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 @@ private Document bindParameters(String source, ConvertingParameterAccessor acces */ 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 @@ protected ParameterBindingDocumentCodec getParameterBindingCodec() { 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 @@ protected CodecRegistry getCodecRegistry() { * @since 2.0.4 */ protected abstract boolean isLimiting(); + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java index e653f1b39a..15ff5e5e23 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java @@ -26,8 +26,15 @@ 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.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 AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongo 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 execute(Object[] parameters) { return method.hasReactiveWrapperParameter() ? executeDeferred(parameters) @@ -269,7 +319,7 @@ Query applyAnnotatedDefaultSortIfPresent(Query query) { 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 @@ private Mono computePipelineStage(String source, MongoPara bsonString -> AbstractReactiveMongoQuery.this.decode(evaluator, bsonString, accessor, codec))); } - private Mono expressionEvaluator(String source, MongoParameterAccessor accessor, - ParameterBindingDocumentCodec codec) { + private Mono> 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 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 @@ protected Mono getParameterBindingCodec() { * @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 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 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 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 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java index 8e3238429d..bef1659308 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java @@ -21,8 +21,10 @@ 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.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 @@ private 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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java index b60b3e6bd1..02084523fb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java @@ -20,11 +20,11 @@ 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 @@ private 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 @@ static Collation computeCollation(@Nullable String collationExpression, Converti 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)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index 26e0dfd89f..33bacb90ff 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -18,6 +18,8 @@ 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.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()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java index c6ad7a634f..8ace6afce3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java @@ -22,11 +22,11 @@ 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 @@ static Query decorateSort(Query query, Document defaultSort) { * @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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java index b181a9fcd9..894f8cdcb5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java @@ -19,6 +19,7 @@ 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.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 ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongo 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. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index 4fdbf711a8..a74694d968 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -22,6 +22,7 @@ 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.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 @@ */ 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 ReactiveStringBasedAggregation(ReactiveMongoQueryMethod method, 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 @@ private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingPar 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); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java index d1f047edd9..565fc8157b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java @@ -21,6 +21,7 @@ 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.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 ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMo * @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 @@ private Mono getBindingContext(String json, ConvertingP ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue, expressionParser); - return getSpelEvaluatorFor(dependencies, accessor) + return getValueExpressionEvaluatorLater(dependencies, accessor) .map(it -> new ParameterBindingContext(accessor::getBindableValue, it)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index 9243ed70d5..1ffca4d85a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -21,6 +21,7 @@ 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.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 StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOper 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 @@ private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingPar 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); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index 325ece597c..a7e7cc38eb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -23,6 +23,8 @@ 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(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java new file mode 100644 index 0000000000..3cefe340f8 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java @@ -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 expressionToContext; + + ValueExpressionDelegateValueExpressionEvaluator(ValueExpressionDelegate delegate, Function expressionToContext) { + this.delegate = delegate; + this.expressionToContext = expressionToContext; + } + + @SuppressWarnings("unchecked") + @Override + public T evaluate(String expressionString) { + ValueExpression expression = delegate.parse(expressionString); + return (T) expression.evaluate(expressionToContext.apply(expression)); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CachingExpressionParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CachingExpressionParser.java deleted file mode 100644 index ccbc9f8de5..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CachingExpressionParser.java +++ /dev/null @@ -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 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"); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 14bbd4af06..f83006ca24 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -42,10 +42,9 @@ 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 @@ */ 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, MongoPersistentProperty> mappingContext; + @Nullable private QueryMethodValueEvaluationContextAccessor accessor; /** * Creates a new {@link MongoRepositoryFactory} with the given {@link MongoOperations}. @@ -148,8 +146,8 @@ protected Object getTargetRepository(RepositoryInformation information) { @Override protected Optional 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 MongoEntityInformation getEntityInformation(Class domainClass) { @@ -170,21 +168,9 @@ private MongoEntityInformation getEntityInformation(Class doma * @author Oliver Gierke * @author Thomas Darimont */ - private static class MongoQueryLookupStrategy implements QueryLookupStrategy { - - private final MongoOperations operations; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; - private final MappingContext, MongoPersistentProperty> mappingContext; - private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER); - - public MongoQueryLookupStrategy(MongoOperations operations, - QueryMethodEvaluationContextProvider evaluationContextProvider, - MappingContext, MongoPersistentProperty> mappingContext) { - - this.operations = operations; - this.evaluationContextProvider = evaluationContextProvider; - this.mappingContext = mappingContext; - } + private record MongoQueryLookupStrategy(MongoOperations operations, + MappingContext, MongoPersistentProperty> mappingContext, + ValueExpressionDelegate expressionSupport) implements QueryLookupStrategy { @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, @@ -197,14 +183,13 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, 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); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 0d3f829b8f..a65740fe33 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -21,6 +21,7 @@ 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.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 @@ */ 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, MongoPersistentProperty> mappingContext; + @Nullable private QueryMethodValueEvaluationContextAccessor accessor; /** * Creates a new {@link ReactiveMongoRepositoryFactory} with the given {@link ReactiveMongoOperations}. @@ -133,13 +132,12 @@ protected Object getTargetRepository(RepositoryInformation information) { return targetRepository; } - @Override - protected Optional getQueryLookupStrategy(@Nullable Key key, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new MongoQueryLookupStrategy(operations, - (ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider, mappingContext)); + @Override protected Optional getQueryLookupStrategy(Key key, + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate)); } + @Override public MongoEntityInformation getEntityInformation(Class domainClass) { return getEntityInformation(domainClass, null); } @@ -160,21 +158,9 @@ private MongoEntityInformation getEntityInformation(Class doma * @author Mark Paluch * @author Christoph Strobl */ - private static class MongoQueryLookupStrategy implements QueryLookupStrategy { - - private final ReactiveMongoOperations operations; - private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider; - private final MappingContext, MongoPersistentProperty> mappingContext; - private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER); - - MongoQueryLookupStrategy(ReactiveMongoOperations operations, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, - MappingContext, MongoPersistentProperty> mappingContext) { - - this.operations = operations; - this.evaluationContextProvider = evaluationContextProvider; - this.mappingContext = mappingContext; - } + private record MongoQueryLookupStrategy(ReactiveMongoOperations operations, + MappingContext, MongoPersistentProperty> mappingContext, + ValueExpressionDelegate delegate) implements QueryLookupStrategy { @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, @@ -187,14 +173,13 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, 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); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonScanner.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonScanner.java index d5a41582c4..37f3a0a60a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonScanner.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonScanner.java @@ -222,7 +222,7 @@ private JsonToken scanBindString() { 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 == '{') { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java index 1ad1f31a37..70eb840b7a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java @@ -26,6 +26,8 @@ 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 ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser */ public ParameterBindingContext(ValueProvider valueProvider, ExpressionParser expressionParser, Supplier 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 EvaluationContext getEvaluationContext(String expressionString) { }); } - /** - * @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 EvaluationContext getEvaluationContext(String expressionString) { }); } + /** + * 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); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java index f3cffaa798..61e0791f95 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java @@ -41,11 +41,11 @@ 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 Document decode(@Nullable String json, ParameterBindingContext bindingCon * @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 @@ static class DependencyCapturingExpressionEvaluator implements ValueExpressionEv private static final Object PLACEHOLDER = new Object(); - private final ExpressionParser expressionParser; + private final ValueExpressionParser expressionParser; private final List dependencies = new ArrayList<>(); - DependencyCapturingExpressionEvaluator(ExpressionParser expressionParser) { + DependencyCapturingExpressionEvaluator(ValueExpressionParser expressionParser) { this.expressionParser = expressionParser; } @@ -400,7 +400,7 @@ static class DependencyCapturingExpressionEvaluator implements ValueExpressionEv @Override public T evaluate(String expression) { - dependencies.add(ExpressionDependencies.discover(expressionParser.parseExpression(expression))); + dependencies.add(expressionParser.parse(expression).getExpressionDependencies()); return (T) PLACEHOLDER; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java index 4ef61835f1..2d1d939242 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java @@ -64,9 +64,9 @@ */ 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 @@ private BindableValue bindableValueFor(JsonToken token) { 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 innerSpelVariables = new HashMap<>(); @@ -385,6 +386,7 @@ private BindableValue bindableValueFor(JsonToken token) { 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 @@ private BindableValue bindableValueFor(JsonToken token) { } } - Object value = evaluateExpression(expression, innerSpelVariables); + Object value = evaluateExpression(expressionString, innerSpelVariables); bindableValue.setValue(value); bindableValue.setType(bsonTypeForValue(value)); return bindableValue; @@ -421,6 +423,7 @@ private BindableValue bindableValueFor(JsonToken token) { 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 innerSpelVariables = new HashMap<>(); @@ -432,6 +435,7 @@ private BindableValue bindableValueFor(JsonToken token) { 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 @@ private BindableValue bindableValueFor(JsonToken token) { } } - 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 @@ private long visitISODateTimeConstructor() { } verifyToken(JsonTokenType.RIGHT_PAREN); - + String dateTimeString = token.getValue(String.class); try { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java index dcedba84ea..15dd600b01 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java @@ -34,12 +34,14 @@ 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 @@ void collectionAllowsReferencingSpringBean() { assertThat(entity.getCollection()).isEqualTo("otherReference"); } + @Test // GH-2764 + void collectionAllowsReferencingProperties() { + + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("collectionName", "reference"); + + BasicMongoPersistentEntity 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 @@ class Company {} @Document("#{@myBean.collectionName}") class DynamicallyMapped {} + @Document("${collectionName}_cat") + class DynamicallyMappedUsingPropertyPlaceholder {} + class CollectionProvider { String collectionName; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java index c35a328a1f..79029812e2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java @@ -20,6 +20,7 @@ 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.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.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.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 @@ private static class MongoQueryFake extends AbstractMongoQuery { 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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java index c524d5edf8..63967080c0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java @@ -22,6 +22,7 @@ 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.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.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 @@ void shouldApplyDynamicAnnotatedCollationWithMultiplePlaceholders() { createQueryForMethod("findWithCollationUsingPlaceholdersInDocumentByFirstName", String.class, String.class, int.class) // - .executeBlocking(new Object[] { "dalinar", "en_US", 2 }); + .executeBlocking(new Object[] { "dalinar", "en_US", 2 }); ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); verify(withQueryMock).matching(captor.capture()); @@ -299,8 +305,12 @@ private static class ReactiveMongoQueryFake extends AbstractReactiveMongoQuery { 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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java index 7859a426f8..61f7dc3d99 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java @@ -19,14 +19,13 @@ 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.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.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 @@ @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 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 void shouldSupportExpressionsInCustomQueries() throws Exception { 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 void shouldSupportNonQuotedBinaryDataReplacement() throws Exception { @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 @@ void shouldConsiderReactiveSpelExtension() throws Exception { 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 { @@ -290,6 +308,9 @@ private interface SampleRepository extends Repository { @Query("{'lastname': ?#{[0]} }") Flux findByQueryWithExpression(String param0); + @Query("{'lastname': ?${foo} }") + Flux findByQueryWithProperty(); + @Query("{'id':?#{ [0] ? { $exists :true} : [1] }}") Flux findByQueryWithExpressionAndNestedObject(boolean param0, String param1); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java index 590b948bcb..03f3b3e5ff 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java @@ -25,6 +25,7 @@ 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.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.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 @@ @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 findOperation; @@ -85,10 +92,14 @@ public class StringBasedMongoQueryUnitTests { MongoConverter converter; + Map 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 void shouldSupportExpressionsInCustomQueries() { 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 @@ private StringBasedMongoQuery createQueryForMethod(String name, Class... para 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 @@ private interface SampleRepository extends Repository { @Query("{'lastname': ?#{[0]} }") List findByQueryWithExpression(String param0); + @Query("{'lastname': ?#{[0]}, 'firstname': ?${absent-property:some-default} }") + List findByQueryWithExpressionAndProperty(String param0); + + @Query("{'lastname': ?${foo} }") + List findByQueryWithProperty(); + @Query("{'id':?#{ [0] ? { $exists :true} : [1] }}") List findByQueryWithExpressionAndNestedObject(boolean param0, String param1); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java index e07a8c4008..646f813481 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java @@ -29,6 +29,9 @@ 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 @@ public TypedValue getRootObject() { }; 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 @@ void discoversNoDependenciesInExpression() { 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 @@ void discoversCorrectlyDependenciesInExpression() { 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 @@ void shouldABindArgumentsViaIndexInSpelExpressions() { 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 @@ void shouldABindArgumentsViaIndexInSpelExpressions() { 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 @@ void shouldABindArgumentsViaIndexInSpelExpressions() { 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 @@ void evaluatesSpelExpressionDefiningEntireQuery() { + ").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 @@ public void capturingExpressionDependenciesShouldNotThrowParseErrorForSpelOnlyJs 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 @@ public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsDocument() { .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 @@ public void throwsExceptionWhenBindEntireQueryUsingSpelExpressionIsMalFormatted( 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 @@ public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsJsonStringCo .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 @@ void bindEntireQueryUsingSpelExpression() { 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 @@ void bindEntireQueryUsingParameter() { 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));