From afdc7912ef468b5f738ba590dcdbf4292ab8faaf Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 30 Sep 2024 15:22:39 +0200 Subject: [PATCH] Added support for ValueExpressions fixes #1522 --- .../repository/query/BindingContext.java | 8 +- .../query/QueryStatementCreator.java | 4 +- .../ReactiveStringBasedCassandraQuery.java | 84 +++++++++++++------ .../query/StringBasedCassandraQuery.java | 69 +++++++++++---- .../repository/query/StringBasedQuery.java | 33 +++++--- ...ssionDelegateValueExpressionEvaluator.java | 41 +++++++++ .../support/CassandraRepositoryFactory.java | 28 ++++--- .../ReactiveCassandraRepositoryFactory.java | 29 ++++--- ...iveStringBasedCassandraQueryUnitTests.java | 32 +++++-- .../StringBasedCassandraQueryUnitTests.java | 33 +++++++- 10 files changed, 270 insertions(+), 91 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/BindingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/BindingContext.java index d13732e38..c52561507 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/BindingContext.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/BindingContext.java @@ -19,7 +19,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.lang.Nullable; @@ -39,13 +39,13 @@ class BindingContext { private final List bindings; - private final SpELExpressionEvaluator evaluator; + private final ValueExpressionEvaluator evaluator; /** * Create new {@link BindingContext}. */ - public BindingContext(CassandraParameters parameters, ParameterAccessor parameterAccessor, - List bindings, SpELExpressionEvaluator evaluator) { + BindingContext(CassandraParameters parameters, ParameterAccessor parameterAccessor, + List bindings, ValueExpressionEvaluator evaluator) { this.parameters = parameters; this.parameterAccessor = parameterAccessor; diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java index 77ad3e3bd..523f45b1c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java @@ -32,7 +32,7 @@ import org.springframework.data.cassandra.repository.Query.Idempotency; import org.springframework.data.domain.Limit; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; @@ -249,7 +249,7 @@ private boolean allowsFiltering() { * @return the {@link Statement}. */ SimpleStatement select(StringBasedQuery stringBasedQuery, CassandraParameterAccessor parameterAccessor, - SpELExpressionEvaluator evaluator) { + ValueExpressionEvaluator evaluator) { try { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java index b00510aac..2669f0fbc 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java @@ -17,11 +17,17 @@ import reactor.core.publisher.Mono; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.repository.Query; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.data.expression.ReactiveValueEvaluationContextProvider; +import org.springframework.data.expression.ValueEvaluationContextProvider; +import org.springframework.data.expression.ValueExpressionParser; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; 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.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -51,8 +57,9 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra private final boolean isExistsQuery; - private final ExpressionParser expressionParser; - private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate delegate; + + private final ReactiveValueEvaluationContextProvider valueEvaluationContextProvider; /** * Create a new {@link ReactiveStringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, @@ -66,7 +73,9 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra * {@link org.springframework.expression.spel.support.StandardEvaluationContext}. * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryMethod * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations + * @deprecated since 4.4, use the constructors accepting {@link ValueExpressionDelegate} instead. */ + @Deprecated(since = "4.4") public ReactiveStringBasedCassandraQuery(ReactiveCassandraQueryMethod queryMethod, ReactiveCassandraOperations operations, ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { @@ -86,19 +95,59 @@ public ReactiveStringBasedCassandraQuery(ReactiveCassandraQueryMethod queryMetho * {@link org.springframework.expression.spel.support.StandardEvaluationContext}. * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryMethod * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations + * @deprecated since 4.4, use the constructors accepting {@link ValueExpressionDelegate} instead. */ + @Deprecated(since = "4.4") public ReactiveStringBasedCassandraQuery(String query, ReactiveCassandraQueryMethod method, ReactiveCassandraOperations operations, ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { + this(query, method, operations, new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser))); + } + + /** + * Create a new {@link ReactiveStringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, + * {@link ReactiveCassandraOperations}, {@link ValueExpressionDelegate} + * + * @param queryMethod {@link ReactiveCassandraQueryMethod} on which this query is based. + * @param operations {@link ReactiveCassandraOperations} used to perform data access in Cassandra. + * @param delegate {@link ValueExpressionDelegate} used to parse expressions in the query. + * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryMethod + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations + * @since 4.4 + */ + public ReactiveStringBasedCassandraQuery(ReactiveCassandraQueryMethod queryMethod, + ReactiveCassandraOperations operations, ValueExpressionDelegate delegate) { + + this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, operations, delegate); + } + + /** + * Create a new {@link ReactiveStringBasedCassandraQuery} for the given {@code query}, {@link CassandraQueryMethod}, + * {@link ReactiveCassandraOperations}, {@link ValueExpressionDelegate} + * + * @param method {@link ReactiveCassandraQueryMethod} on which this query is based. + * @param operations {@link ReactiveCassandraOperations} used to perform data access in Cassandra. + * @param delegate {@link SpelExpressionParser} used to parse expressions in the query. + * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryMethod + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations + * @since 4.4 + */ + public ReactiveStringBasedCassandraQuery(String query, ReactiveCassandraQueryMethod method, + ReactiveCassandraOperations operations, ValueExpressionDelegate delegate) { + super(method, operations); Assert.hasText(query, "Query must not be empty"); - this.expressionParser = expressionParser; - this.evaluationContextProvider = evaluationContextProvider; + this.delegate = delegate; - this.stringBasedQuery = new StringBasedQuery(query, method.getParameters(), expressionParser); + this.stringBasedQuery = new StringBasedQuery(query, method.getParameters(), delegate); + + ValueEvaluationContextProvider valueContextProvider = delegate.createValueContextProvider( + method.getParameters()); + Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, "ValueEvaluationContextProvider must be reactive"); + this.valueEvaluationContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider; if (method.hasAnnotatedQuery()) { @@ -126,10 +175,9 @@ public Mono createQuery(CassandraParameterAccessor parameterAcc StringBasedQuery query = getStringBasedQuery(); ConvertingParameterAccessor parameterAccessorToUse = new ConvertingParameterAccessor( getReactiveCassandraOperations().getConverter(), parameterAccessor); - Mono spelEvaluator = getSpelEvaluatorFor(query.getExpressionDependencies(), - parameterAccessorToUse); - return spelEvaluator.map(it -> getQueryStatementCreator().select(query, parameterAccessorToUse, it)); + return getValueExpressionEvaluatorLater(query.getExpressionDependencies(), parameterAccessor) + .map(it -> getQueryStatementCreator().select(query, parameterAccessorToUse, it)); } @Override @@ -152,21 +200,9 @@ protected boolean isModifyingQuery() { return false; } - /** - * Obtain a {@link Mono publisher} emitting the {@link SpELExpressionEvaluator} 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 SpELExpressionEvaluator} when ready. - */ - private Mono getSpelEvaluatorFor(ExpressionDependencies dependencies, + private Mono getValueExpressionEvaluatorLater(ExpressionDependencies dependencies, CassandraParameterAccessor accessor) { - - return evaluationContextProvider - .getEvaluationContextLater(getQueryMethod().getParameters(), accessor.getValues(), dependencies) - .map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(expressionParser, - evaluationContext)) - .defaultIfEmpty(DefaultSpELExpressionEvaluator.unsupported()); + return valueEvaluationContextProvider.getEvaluationContextLater(accessor.getValues(), dependencies) + .map(evaluationContext -> new ValueExpressionDelegateValueExpressionEvaluator(delegate, valueExpression -> evaluationContext)); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java index c2ef4a655..e04682a9a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java @@ -15,10 +15,14 @@ */ package org.springframework.data.cassandra.repository.query; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.expression.ValueEvaluationContext; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.EvaluationContext; +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; @@ -46,8 +50,7 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { private final boolean isExistsQuery; - private final ExpressionParser expressionParser; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; /** * Create a new {@link StringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, @@ -60,35 +63,52 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { * {@link org.springframework.expression.spel.support.StandardEvaluationContext}. * @see org.springframework.data.cassandra.repository.query.CassandraQueryMethod * @see org.springframework.data.cassandra.core.CassandraOperations + * @deprecated use the constructor version with {@link ValueExpressionDelegate} */ + @Deprecated(since = "4.4") public StringBasedCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations, ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider); } + /** + * Create a new {@link StringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, + * {@link CassandraOperations}, {@link ValueExpressionDelegate}. + * + * @param queryMethod {@link CassandraQueryMethod} on which this query is based. + * @param operations {@link CassandraOperations} used to perform data access in Cassandra. + * @param valueExpressionDelegate {@link ValueExpressionDelegate} used to parse expressions in the query. + * @see org.springframework.data.cassandra.repository.query.CassandraQueryMethod + * @see org.springframework.data.cassandra.core.CassandraOperations + * @since 4.4 + */ + public StringBasedCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations, + ValueExpressionDelegate valueExpressionDelegate) { + + this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, operations, valueExpressionDelegate); + } + /** * Create a new {@link StringBasedCassandraQuery} for the given {@code query}, {@link CassandraQueryMethod}, - * {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. + * {@link CassandraOperations}, {@link ValueExpressionDelegate}. * * @param query {@link String} containing the Apache Cassandra CQL query to execute. * @param method {@link CassandraQueryMethod} on which this query is based. * @param operations {@link CassandraOperations} used to perform data access in Cassandra. - * @param expressionParser {@link SpelExpressionParser} used to parse expressions in the query. - * @param evaluationContextProvider {@link QueryMethodEvaluationContextProvider} used to access the potentially shared - * {@link org.springframework.expression.spel.support.StandardEvaluationContext}. + * @param valueExpressionDelegate {@link ValueExpressionDelegate} used to parse expressions in the query. * @see org.springframework.data.cassandra.repository.query.CassandraQueryMethod * @see org.springframework.data.cassandra.core.CassandraOperations + * @since 4.4 */ public StringBasedCassandraQuery(String query, CassandraQueryMethod method, CassandraOperations operations, - ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + ValueExpressionDelegate valueExpressionDelegate) { super(method, operations); - this.expressionParser = expressionParser; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; - this.stringBasedQuery = new StringBasedQuery(query, method.getParameters(), expressionParser); + this.stringBasedQuery = new StringBasedQuery(query, method.getParameters(), valueExpressionDelegate); if (method.hasAnnotatedQuery()) { @@ -106,6 +126,26 @@ public StringBasedCassandraQuery(String query, CassandraQueryMethod method, Cass } } + /** + * Create a new {@link StringBasedCassandraQuery} for the given {@code query}, {@link CassandraQueryMethod}, + * {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. + * + * @param query {@link String} containing the Apache Cassandra CQL query to execute. + * @param method {@link CassandraQueryMethod} on which this query is based. + * @param operations {@link CassandraOperations} used to perform data access in Cassandra. + * @param expressionParser {@link SpelExpressionParser} used to parse expressions in the query. + * @param evaluationContextProvider {@link QueryMethodEvaluationContextProvider} used to access the potentially shared + * {@link org.springframework.expression.spel.support.StandardEvaluationContext}. + * @see org.springframework.data.cassandra.repository.query.CassandraQueryMethod + * @see org.springframework.data.cassandra.core.CassandraOperations + * @deprecated use the constructor version with {@link ValueExpressionDelegate} + */ + @Deprecated(since = "4.4") + public StringBasedCassandraQuery(String query, CassandraQueryMethod method, CassandraOperations operations, + ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + this(query, method, operations, new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser))); + } + protected StringBasedQuery getStringBasedQuery() { return this.stringBasedQuery; } @@ -116,11 +156,10 @@ public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) StringBasedQuery query = getStringBasedQuery(); ConvertingParameterAccessor parameterAccessorToUse = new ConvertingParameterAccessor(getOperations().getConverter(), parameterAccessor); - EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext( - getQueryMethod().getParameters(), parameterAccessorToUse.getValues(), query.getExpressionDependencies()); + ValueEvaluationContext evaluationContext = valueExpressionDelegate.createValueContextProvider( + getQueryMethod().getParameters()).getEvaluationContext(parameterAccessorToUse.getValues(), query.getExpressionDependencies()); - return getQueryStatementCreator().select(query, parameterAccessorToUse, - new DefaultSpELExpressionEvaluator(expressionParser, evaluationContext)); + return getQueryStatementCreator().select(query, parameterAccessorToUse, new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, valueExpression -> evaluationContext)); } @Override diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedQuery.java index 14f1a9968..47973467a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedQuery.java @@ -17,14 +17,15 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.data.cassandra.repository.query.BindingContext.ParameterBinding; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.expression.ExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -43,21 +44,20 @@ class StringBasedQuery { private final CassandraParameters parameters; - private final ExpressionParser expressionParser; + private final ValueExpressionDelegate expressionParser; private final List queryParameterBindings = new ArrayList<>(); private final ExpressionDependencies expressionDependencies; /** - * Create a new {@link StringBasedQuery} given {@code query}, {@link CassandraParameters} and - * {@link ExpressionParser}. + * Create a new {@link StringBasedQuery} given {@code query}, {@link CassandraParameters} and {@link ValueExpressionDelegate}. * * @param query must not be empty. * @param parameters must not be {@literal null}. * @param expressionParser must not be {@literal null}. */ - StringBasedQuery(String query, CassandraParameters parameters, ExpressionParser expressionParser) { + StringBasedQuery(String query, CassandraParameters parameters, ValueExpressionDelegate expressionParser) { this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, this.queryParameterBindings); @@ -77,7 +77,7 @@ private ExpressionDependencies createExpressionDependencies() { for (ParameterBinding binding : queryParameterBindings) { if (binding.isExpression()) { dependencies - .add(ExpressionDependencies.discover(expressionParser.parseExpression(binding.getRequiredExpression()))); + .add(expressionParser.parse(binding.getRequiredExpression()).getExpressionDependencies()); } } @@ -100,7 +100,7 @@ public ExpressionDependencies getExpressionDependencies() { * @param evaluator must not be {@literal null}. * @return the bound String query containing formatted parameters. */ - public SimpleStatement bindQuery(CassandraParameterAccessor parameterAccessor, SpELExpressionEvaluator evaluator) { + SimpleStatement bindQuery(CassandraParameterAccessor parameterAccessor, ValueExpressionEvaluator evaluator) { Assert.notNull(parameterAccessor, "CassandraParameterAccessor must not be null"); Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null"); @@ -176,6 +176,10 @@ enum ParameterBindingParser { private static final Pattern NAMED_PARAMETER_BINDING_PATTERN = Pattern.compile("\\:(\\w+)"); private static final Pattern INDEX_BASED_EXPRESSION_PATTERN = Pattern.compile("\\?\\#\\{"); private static final Pattern NAME_BASED_EXPRESSION_PATTERN = Pattern.compile("\\:\\#\\{"); + private static final Pattern INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN = Pattern.compile("\\?\\$\\{"); + private static final Pattern NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN = Pattern.compile("\\:\\$\\{"); + + private static final Set VALUE_EXPRESSION_PATTERNS = Set.of(INDEX_BASED_EXPRESSION_PATTERN, NAME_BASED_EXPRESSION_PATTERN, INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN, NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN); private static final String ARGUMENT_PLACEHOLDER = "?_param_?"; @@ -217,7 +221,7 @@ private static String transformQueryAndCollectExpressionParametersIntoBindings(S int exprStart = matcher.start(); currentPosition = exprStart; - if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) { + if (isValueExpression(matcher)) { // eat parameter expression int curlyBraceOpenCount = 1; currentPosition += 3; @@ -233,7 +237,6 @@ private static String transformQueryAndCollectExpressionParametersIntoBindings(S default: } } - result.append(input.subSequence(startIndex, exprStart)); } else { result.append(input.subSequence(startIndex, exprStart)); @@ -241,10 +244,10 @@ private static String transformQueryAndCollectExpressionParametersIntoBindings(S result.append(ARGUMENT_PLACEHOLDER); - if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) { + if (isValueExpression(matcher)) { bindings.add( BindingContext.ParameterBinding - .expression(input.substring(exprStart + 3, currentPosition - 1), true)); + .expression(input.substring(exprStart + 1, currentPosition), true)); } else { if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) { bindings @@ -262,6 +265,10 @@ private static String transformQueryAndCollectExpressionParametersIntoBindings(S return result.append(input.subSequence(currentPosition, input.length())).toString(); } + private static boolean isValueExpression(Matcher matcher) { + return VALUE_EXPRESSION_PATTERNS.contains(matcher.pattern()); + } + @Nullable private static Matcher findNextBindingOrExpression(String input, int position) { @@ -271,6 +278,8 @@ private static Matcher findNextBindingOrExpression(String input, int position) { matchers.add(NAMED_PARAMETER_BINDING_PATTERN.matcher(input)); matchers.add(INDEX_BASED_EXPRESSION_PATTERN.matcher(input)); matchers.add(NAME_BASED_EXPRESSION_PATTERN.matcher(input)); + matchers.add(INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN.matcher(input)); + matchers.add(NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN.matcher(input)); TreeMap matcherMap = new TreeMap<>(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java new file mode 100644 index 000000000..1ee1073d8 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/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.cassandra.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-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java index e235dbd62..f45642160 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; @@ -27,6 +28,7 @@ import org.springframework.data.cassandra.repository.query.CassandraQueryMethod; import org.springframework.data.cassandra.repository.query.PartTreeCassandraQuery; import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; @@ -36,8 +38,9 @@ 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.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -53,7 +56,7 @@ */ public class CassandraRepositoryFactory extends RepositoryFactorySupport { - private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + private static final ValueExpressionParser EXPRESSION_PARSER = ValueExpressionParser.create(SpelExpressionParser::new); private final MappingContext, CassandraPersistentProperty> mappingContext; @@ -99,29 +102,31 @@ public CassandraEntityInformation getEntityInformation(Class d return new MappingCassandraEntityInformation<>((CassandraPersistentEntity) entity, operations.getConverter()); } + @Override protected Optional getQueryLookupStrategy(Key key, + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new CassandraQueryLookupStrategy(operations, valueExpressionDelegate, mappingContext)); + } + @Override protected Optional getQueryLookupStrategy(@Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { - - return Optional.of(new CassandraQueryLookupStrategy(operations, evaluationContextProvider, mappingContext)); + return Optional.of(new CassandraQueryLookupStrategy(operations, new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), EXPRESSION_PARSER), mappingContext)); } private static class CassandraQueryLookupStrategy implements QueryLookupStrategy { - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; private final MappingContext, CassandraPersistentProperty> mappingContext; private final CassandraOperations operations; - private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER); - CassandraQueryLookupStrategy(CassandraOperations operations, - QueryMethodEvaluationContextProvider evaluationContextProvider, + ValueExpressionDelegate valueExpressionDelegate, MappingContext, CassandraPersistentProperty> mappingContext) { this.operations = operations; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; this.mappingContext = mappingContext; } @@ -134,10 +139,9 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringBasedCassandraQuery(namedQuery, queryMethod, operations, expressionParser, - evaluationContextProvider); + return new StringBasedCassandraQuery(namedQuery, queryMethod, operations, valueExpressionDelegate); } else if (queryMethod.hasAnnotatedQuery()) { - return new StringBasedCassandraQuery(queryMethod, operations, expressionParser, evaluationContextProvider); + return new StringBasedCassandraQuery(queryMethod, operations, valueExpressionDelegate); } else { return new PartTreeCassandraQuery(queryMethod, operations); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java index f95efb244..71813ae63 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; @@ -26,6 +27,7 @@ import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryMethod; import org.springframework.data.cassandra.repository.query.ReactivePartTreeCassandraQuery; import org.springframework.data.cassandra.repository.query.ReactiveStringBasedCassandraQuery; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; @@ -35,9 +37,10 @@ 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.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,7 +53,7 @@ */ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactorySupport { - private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + private static final ValueExpressionParser EXPRESSION_PARSER = ValueExpressionParser.create(SpelExpressionParser::new); private final ReactiveCassandraOperations operations; @@ -89,11 +92,15 @@ protected Object getTargetRepository(RepositoryInformation information) { return getTargetRepositoryViaReflection(information, entityInformation, operations); } + @Override protected Optional getQueryLookupStrategy(Key key, + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new CassandraQueryLookupStrategy(operations, valueExpressionDelegate, mappingContext)); + } + @Override protected Optional getQueryLookupStrategy(@Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new CassandraQueryLookupStrategy(operations, - (ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider, mappingContext)); + return Optional.of(new CassandraQueryLookupStrategy(operations, new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), EXPRESSION_PARSER), mappingContext)); } @SuppressWarnings("unchecked") @@ -112,19 +119,17 @@ public CassandraEntityInformation getEntityInformation(Class d */ private static class CassandraQueryLookupStrategy implements QueryLookupStrategy { - private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate delegate; private final ReactiveCassandraOperations operations; private final MappingContext, ? extends CassandraPersistentProperty> mappingContext; - private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER); - CassandraQueryLookupStrategy(ReactiveCassandraOperations operations, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, + ValueExpressionDelegate delegate, MappingContext, ? extends CassandraPersistentProperty> mappingContext) { - this.evaluationContextProvider = evaluationContextProvider; + this.delegate = delegate; this.operations = operations; this.mappingContext = mappingContext; } @@ -141,11 +146,9 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new ReactiveStringBasedCassandraQuery(namedQuery, queryMethod, operations, expressionParser, - evaluationContextProvider); + return new ReactiveStringBasedCassandraQuery(namedQuery, queryMethod, operations, delegate); } else if (queryMethod.hasAnnotatedQuery()) { - return new ReactiveStringBasedCassandraQuery(queryMethod, operations, expressionParser, - evaluationContextProvider); + return new ReactiveStringBasedCassandraQuery(queryMethod, operations, delegate); } else { return new ReactivePartTreeCassandraQuery(queryMethod, operations); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java index 9ff84fd38..00a6db7e4 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.data.cassandra.ReactiveSession; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; @@ -37,16 +38,20 @@ import org.springframework.data.cassandra.domain.Person; import org.springframework.data.cassandra.repository.Consistency; import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; +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; import org.springframework.lang.Nullable; +import org.springframework.mock.env.MockEnvironment; import org.springframework.util.ReflectionUtils; import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; @@ -60,7 +65,7 @@ @ExtendWith(MockitoExtension.class) class ReactiveStringBasedCassandraQueryUnitTests { - private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new); @Mock private ReactiveCassandraOperations operations; @Mock private ReactiveCqlOperations cqlOperations; @@ -69,6 +74,7 @@ class ReactiveStringBasedCassandraQueryUnitTests { private MappingCassandraConverter converter; private ProjectionFactory factory; private RepositoryMetadata metadata; + private MockEnvironment mockEnvironment = new MockEnvironment(); @BeforeEach @SuppressWarnings("unchecked") @@ -143,6 +149,20 @@ void shouldUseSpelExtension() { assertThat(actual.getPositionalValues().get(0)).isEqualTo("Walter"); } + @Test // GH-1522 + void shouldUsePropertyPlaceholder() { + mockEnvironment.withProperty("someProp", "Walter"); + ReactiveStringBasedCassandraQuery cassandraQuery = getQueryMethod("findByPropertyPlaceholder"); + + CassandraParametersParameterAccessor parameterAccessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod()); + + SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor).block(); + + assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname=?;"); + assertThat(actual.getPositionalValues().get(0)).isEqualTo("Walter"); + } + private ReactiveStringBasedCassandraQuery getQueryMethod(String name, Class... args) { Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args); @@ -150,11 +170,10 @@ private ReactiveStringBasedCassandraQuery getQueryMethod(String name, Class.. ReactiveCassandraQueryMethod queryMethod = new ReactiveCassandraQueryMethod(method, metadata, factory, converter.getMappingContext()); - ReactiveExtensionAwareQueryMethodEvaluationContextProvider provider = new ReactiveExtensionAwareQueryMethodEvaluationContextProvider( - Arrays.asList(MyReactiveExtension.INSTANCE, MyDefunctExtension.INSTANCE)); + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor( + mockEnvironment, Arrays.asList(MyReactiveExtension.INSTANCE, MyDefunctExtension.INSTANCE)); - return new ReactiveStringBasedCassandraQuery(queryMethod, operations, PARSER, - provider); + return new ReactiveStringBasedCassandraQuery(queryMethod, operations, new ValueExpressionDelegate(accessor, PARSER)); } @SuppressWarnings("unused") @@ -170,6 +189,9 @@ private interface SampleRepository extends Repository { @Query("SELECT * FROM person WHERE lastname=:#{getName()};") Person findBySpel(); + @Query("SELECT * FROM person WHERE lastname=:${someProp};") + Person findByPropertyPlaceholder(); + } public static class MyReactiveExtensionObject implements EvaluationContextExtension { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java index 71df5439a..3179afaa1 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java @@ -25,6 +25,7 @@ import java.time.LocalDate; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -33,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.cql.QueryOptions; @@ -43,15 +45,18 @@ import org.springframework.data.cassandra.repository.Consistency; import org.springframework.data.cassandra.repository.Query; import org.springframework.data.cassandra.support.UserDefinedTypeBuilder; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; -import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.QueryCreationException; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.mock.env.MockEnvironment; import org.springframework.util.ReflectionUtils; import com.datastax.oss.driver.api.core.CqlIdentifier; @@ -71,7 +76,7 @@ @ExtendWith(MockitoExtension.class) class StringBasedCassandraQueryUnitTests { - private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new); @Mock private CassandraOperations operations; @Mock private UdtValue udtValue; @@ -80,6 +85,7 @@ class StringBasedCassandraQueryUnitTests { private RepositoryMetadata metadata; private MappingCassandraConverter converter; private ProjectionFactory factory; + private MockEnvironment environment = new MockEnvironment(); @BeforeEach void setUp() { @@ -293,6 +299,20 @@ void bindsConditionalExpressionParameterCorrectly() { assertThat(actual.getPositionalValues().get(0)).isEqualTo("Walter"); } + @Test // GH-1522 + void bindsPropertyPlaceholderParameterCorrectly() { + environment.withProperty("someParam", "Walter"); + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByPropertyPlaceholder"); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod()); + + SimpleStatement actual = cassandraQuery.createQuery(accessor); + + assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getPositionalValues().get(0)).isEqualTo("Walter"); + } + @Test // DATACASS-117 void bindsReusedParametersCorrectly() { @@ -431,8 +451,10 @@ private StringBasedCassandraQuery getQueryMethod(String name, Class... args) CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory, converter.getMappingContext()); - return new StringBasedCassandraQuery(queryMethod, operations, PARSER, - ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT); + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor( + environment, Collections.emptySet()); + + return new StringBasedCassandraQuery(queryMethod, operations, new ValueExpressionDelegate(accessor, PARSER)); } @SuppressWarnings("unused") @@ -480,6 +502,9 @@ private interface SampleRepository extends Repository { @Query("SELECT * FROM person WHERE lastname = :#{#lastname == 'Matthews' ? 'Woohoo' : #lastname};") Person findByConditionalExpressionParameter(@Param("lastname") String lastname); + @Query("SELECT * FROM person WHERE lastname = :${someParam};") + Person findByPropertyPlaceholder(); + @Query("SELECT * FROM person WHERE lastname=?0 AND firstname=?1;") Person findByLastnameAndFirstname(String lastname, String firstname);