From e9801aaf9aa09c9d59a275e49b351c59d338cd8f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 27 Jul 2020 10:48:24 +0200 Subject: [PATCH] DATACASS-788 - Add support for reactive SpEL context extensions. We now support context extensions that contribute contextual details using a reactive programming model for evaluation of SpEL expressions in @Query methods. --- .../query/AbstractReactiveCassandraQuery.java | 19 +- .../repository/query/BindingContext.java | 174 ++++++++++++ .../query/DefaultSpELExpressionEvaluator.java | 72 +++++ .../ExpressionEvaluatingParameterBinder.java | 250 ------------------ .../query/QueryStatementCreator.java | 6 +- .../ReactiveCassandraQueryExecution.java | 14 +- .../query/ReactivePartTreeCassandraQuery.java | 29 +- .../ReactiveStringBasedCassandraQuery.java | 51 +++- .../query/StringBasedCassandraQuery.java | 23 +- .../repository/query/StringBasedQuery.java | 79 ++++-- .../support/CachingExpressionParser.java | 59 +++++ .../support/CassandraRepositoryFactory.java | 10 +- .../ReactiveCassandraRepositoryFactory.java | 19 +- ...eactiveCassandraRepositoryFactoryBean.java | 15 ++ ...veCassandraRepositoryIntegrationTests.java | 1 - .../ParameterBindingParserUnitTests.java | 2 +- ...activePartTreeCassandraQueryUnitTests.java | 3 +- ...iveStringBasedCassandraQueryUnitTests.java | 104 +++++++- ...veCassandraRepositoryIntegrationTests.java | 1 - src/main/asciidoc/new-features.adoc | 1 + 20 files changed, 590 insertions(+), 342 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/BindingContext.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/DefaultSpELExpressionEvaluator.java delete mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CachingExpressionParser.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java index 175888c2d..afd31ffe6 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java @@ -38,7 +38,6 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.util.Assert; import com.datastax.oss.driver.api.core.cql.SimpleStatement; -import com.datastax.oss.driver.api.core.cql.Statement; /** * Base class for reactive {@link RepositoryQuery} implementations for Cassandra. @@ -81,18 +80,10 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository */ @Override public Object execute(Object[] parameters) { - - return getQueryMethod().hasReactiveWrapperParameter() ? executeDeferred(parameters) : executeNow(parameters); + return Flux.defer(() -> executeLater(parameters)); } - @SuppressWarnings("unchecked") - private Object executeDeferred(Object[] parameters) { - - return getQueryMethod().isCollectionQuery() ? Flux.defer(() -> (Publisher) execute(parameters)) - : Mono.defer(() -> (Mono) execute(parameters)); - } - - private Object executeNow(Object[] parameters) { + private Publisher executeLater(Object[] parameters) { ReactiveCassandraParameterAccessor parameterAccessor = new ReactiveCassandraParameterAccessor(getQueryMethod(), parameters); @@ -100,7 +91,7 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository CassandraParameterAccessor convertingParameterAccessor = new ConvertingParameterAccessor( getRequiredConverter(getReactiveCassandraOperations()), parameterAccessor); - Statement statement = createQuery(convertingParameterAccessor); + Mono statement = createQuery(convertingParameterAccessor); ResultProcessor resultProcessor = getQueryMethod().getResultProcessor() .withDynamicProjection(convertingParameterAccessor); @@ -110,7 +101,7 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository Class resultType = resolveResultType(resultProcessor); - return queryExecution.execute(statement, resultType); + return statement.flatMapMany(it -> queryExecution.execute(it, resultType)); } private Class resolveResultType(ResultProcessor resultProcessor) { @@ -126,7 +117,7 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository * * @param accessor must not be {@literal null}. */ - protected abstract SimpleStatement createQuery(CassandraParameterAccessor accessor); + protected abstract Mono createQuery(CassandraParameterAccessor accessor); protected ReactiveCassandraOperations getReactiveCassandraOperations() { return this.operations; 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 new file mode 100644 index 000000000..036eafda1 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/BindingContext.java @@ -0,0 +1,174 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Value object capturing the binding context to provide {@link #getBindingValues() binding values} for queries. + * + * @author Mark Paluch + * @since 1.5 + */ +class BindingContext { + + private final CassandraParameters parameters; + + private final ParameterAccessor parameterAccessor; + + private final List bindings; + + private final SpELExpressionEvaluator evaluator; + + /** + * Create new {@link BindingContext}. + */ + public BindingContext(CassandraParameters parameters, ParameterAccessor parameterAccessor, + List bindings, SpELExpressionEvaluator evaluator) { + + this.parameters = parameters; + this.parameterAccessor = parameterAccessor; + this.bindings = bindings; + this.evaluator = evaluator; + } + + /** + * @return {@literal true} when list of bindings is not empty. + */ + private boolean hasBindings() { + return !bindings.isEmpty(); + } + + /** + * Bind values provided by {@link CassandraParameterAccessor} to placeholders in {@link BindingContext} while + * considering potential conversions and parameter types. + * + * @return {@literal null} if given {@code raw} value is empty. + */ + public List getBindingValues() { + + if (!hasBindings()) { + return Collections.emptyList(); + } + + List parameters = new ArrayList<>(bindings.size()); + + for (ParameterBinding binding : bindings) { + Object parameterValueForBinding = getParameterValueForBinding(binding); + parameters.add(parameterValueForBinding); + } + + return parameters; + } + + /** + * Return the value to be used for the given {@link ParameterBinding}. + * + * @param binding must not be {@literal null}. + * @return the value used for the given {@link ParameterBinding}. + */ + @Nullable + private Object getParameterValueForBinding(ParameterBinding binding) { + + if (binding.isExpression()) { + return evaluator.evaluate(binding.getRequiredExpression()); + } + + return binding.isNamed() + ? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName())) + : parameterAccessor.getBindableValue(binding.getParameterIndex()); + } + + private int getParameterIndex(CassandraParameters parameters, String parameterName) { + + return parameters.stream() // + .filter(cassandraParameter -> cassandraParameter // + .getName().filter(s -> s.equals(parameterName)) // + .isPresent()) // + .mapToInt(Parameter::getIndex) // + .findFirst() // + .orElseThrow(() -> new IllegalArgumentException( + String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName))); + } + + /** + * A generic parameter binding with name or position information. + * + * @author Mark Paluch + */ + static class ParameterBinding { + + private final int parameterIndex; + private final @Nullable String expression; + private final @Nullable String parameterName; + + private ParameterBinding(int parameterIndex, @Nullable String expression, @Nullable String parameterName) { + + this.parameterIndex = parameterIndex; + this.expression = expression; + this.parameterName = parameterName; + } + + static ParameterBinding expression(String expression, boolean quoted) { + return new ParameterBinding(-1, expression, null); + } + + static ParameterBinding indexed(int parameterIndex) { + return new ParameterBinding(parameterIndex, null, null); + } + + static ParameterBinding named(String name) { + return new ParameterBinding(-1, null, name); + } + + boolean isNamed() { + return (parameterName != null); + } + + int getParameterIndex() { + return parameterIndex; + } + + String getParameter() { + return ("?" + (isExpression() ? "expr" : "") + parameterIndex); + } + + String getRequiredExpression() { + + Assert.state(expression != null, "ParameterBinding is not an expression"); + return expression; + } + + boolean isExpression() { + return (this.expression != null); + } + + String getRequiredParameterName() { + + Assert.state(parameterName != null, "ParameterBinding is not named"); + + return parameterName; + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/DefaultSpELExpressionEvaluator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/DefaultSpELExpressionEvaluator.java new file mode 100644 index 000000000..1c782813e --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/DefaultSpELExpressionEvaluator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; + +/** + * Simple {@link SpELExpressionEvaluator} implementation using {@link ExpressionParser} and {@link EvaluationContext}. + * + * @author Mark Paluch + * @since 3.1 + */ +class DefaultSpELExpressionEvaluator implements SpELExpressionEvaluator { + + private final ExpressionParser parser; + + private final EvaluationContext context; + + DefaultSpELExpressionEvaluator(ExpressionParser parser, EvaluationContext context) { + this.parser = parser; + this.context = context; + } + + /** + * Return a {@link SpELExpressionEvaluator} that does not support expression evaluation. + * + * @return a {@link SpELExpressionEvaluator} that does not support expression evaluation. + */ + public static SpELExpressionEvaluator unsupported() { + return NoOpExpressionEvaluator.INSTANCE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.SpELExpressionEvaluator#evaluate(java.lang.String) + */ + @Override + @SuppressWarnings("unchecked") + public T evaluate(String expression) { + return (T) parser.parseExpression(expression).getValue(context, Object.class); + } + + /** + * {@link SpELExpressionEvaluator} that does not support SpEL evaluation. + * + * @author Mark Paluch + */ + enum NoOpExpressionEvaluator implements SpELExpressionEvaluator { + + INSTANCE; + + @Override + public T evaluate(String expression) { + throw new UnsupportedOperationException("Expression evaluation not supported"); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java deleted file mode 100644 index 4814e45a2..000000000 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2016-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.cassandra.repository.query; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.springframework.data.repository.query.Parameter; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; - -/** - * {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a - * {@link String}. - * - * @author Mark Paluch - * @since 1.5 - */ -class ExpressionEvaluatingParameterBinder { - - private final SpelExpressionParser expressionParser; - - private final QueryMethodEvaluationContextProvider evaluationContextProvider; - - /** - * Creates new {@link ExpressionEvaluatingParameterBinder} - * - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - */ - ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - - Assert.notNull(expressionParser, "ExpressionParser must not be null"); - Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null"); - - this.expressionParser = expressionParser; - this.evaluationContextProvider = evaluationContextProvider; - } - - /** - * Bind values provided by {@link CassandraParameterAccessor} to placeholders in {@link BindingContext} while - * considering potential conversions and parameter types. - * - * @param parameterAccessor must not be {@literal null}. - * @param bindingContext must not be {@literal null}. - * @return {@literal null} if given {@code raw} value is empty. - */ - public List bind(CassandraParameterAccessor parameterAccessor, BindingContext bindingContext) { - - if (!bindingContext.hasBindings()) { - return Collections.emptyList(); - } - - List parameters = new ArrayList<>(bindingContext.getBindings().size()); - - bindingContext.getBindings() // - .stream() // - .map(binding -> getParameterValueForBinding(parameterAccessor, bindingContext.getParameters(), binding)) // - .forEach(parameters::add); - - return parameters; - } - - /** - * Returns the value to be used for the given {@link ParameterBinding}. - * - * @param parameterAccessor must not be {@literal null}. - * @param parameters must not be {@literal null}. - * @param binding must not be {@literal null}. - * @return the value used for the given {@link ParameterBinding}. - */ - @Nullable - private Object getParameterValueForBinding(CassandraParameterAccessor parameterAccessor, - CassandraParameters parameters, ParameterBinding binding) { - - if (binding.isExpression()) { - return evaluateExpression(binding.getExpression(), parameters, parameterAccessor.getValues()); - } - - return binding.isNamed() - ? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getParameterName())) - : parameterAccessor.getBindableValue(binding.getParameterIndex()); - } - - private int getParameterIndex(CassandraParameters parameters, String parameterName) { - - return parameters.stream() // - .filter(cassandraParameter -> cassandraParameter // - .getName().filter(s -> s.equals(parameterName)) // - .isPresent()) // - .mapToInt(Parameter::getIndex) // - .findFirst() // - .orElseThrow(() -> new IllegalArgumentException( - String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName))); - } - - /** - * Evaluates the given {@code expressionString}. - * - * @param expressionString must not be {@literal null} or empty. - * @param parameters must not be {@literal null}. - * @param parameterValues must not be {@literal null}. - * @return the value of the {@code expressionString} evaluation. - */ - @Nullable - private Object evaluateExpression(String expressionString, CassandraParameters parameters, Object[] parameterValues) { - - EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues); - Expression expression = expressionParser.parseExpression(expressionString); - - return expression.getValue(evaluationContext, Object.class); - } - - /** - * @author Mark Paluch - * @since 1.5 - */ - static class BindingContext { - - final CassandraQueryMethod queryMethod; - final List bindings; - - /** - * Creates new {@link BindingContext}. - * - * @param queryMethod {@link CassandraQueryMethod} on which the parameters are evaluated. - * @param bindings {@link List} of {@link ParameterBinding} containing name or position (index) information - * pertaining to the parameter in the referenced {@code queryMethod}. - */ - public BindingContext(CassandraQueryMethod queryMethod, List bindings) { - - this.queryMethod = queryMethod; - this.bindings = bindings; - } - - /** - * @return {@literal true} when list of bindings is not empty. - */ - boolean hasBindings() { - return !CollectionUtils.isEmpty(bindings); - } - - /** - * Get unmodifiable list of {@link ParameterBinding}s. - * - * @return never {@literal null}. - */ - List getBindings() { - return Collections.unmodifiableList(bindings); - } - - /** - * Get the associated {@link CassandraParameters}. - * - * @return the {@link CassandraParameters} associated with the {@link CassandraQueryMethod}. - */ - CassandraParameters getParameters() { - return queryMethod.getParameters(); - } - - /** - * Get the {@link CassandraQueryMethod}. - * - * @return the {@link CassandraQueryMethod} used in the expression evaluation context. - */ - CassandraQueryMethod getQueryMethod() { - return queryMethod; - } - } - - /** - * A generic parameter binding with name or position information. - * - * @author Mark Paluch - */ - static class ParameterBinding { - - private final boolean quoted; - private final int parameterIndex; - private final @Nullable String expression; - private final @Nullable String parameterName; - - private ParameterBinding(int parameterIndex, boolean quoted, @Nullable String expression, - @Nullable String parameterName) { - - this.parameterIndex = parameterIndex; - this.quoted = quoted; - this.expression = expression; - this.parameterName = parameterName; - } - - static ParameterBinding expression(String expression, boolean quoted) { - return new ParameterBinding(-1, quoted, expression, null); - } - - static ParameterBinding indexed(int parameterIndex) { - return new ParameterBinding(parameterIndex, false, null, null); - } - - static ParameterBinding named(String name) { - return new ParameterBinding(-1, false, null, name); - } - - boolean isNamed() { - return (parameterName != null); - } - - int getParameterIndex() { - return parameterIndex; - } - - String getParameter() { - return ("?" + (isExpression() ? "expr" : "") + parameterIndex); - } - - @Nullable - String getExpression() { - return expression; - } - - boolean isExpression() { - return (this.expression != null); - } - - @Nullable - String getParameterName() { - return parameterName; - } - } -} 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 5bbe45741..83135a831 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 @@ -31,6 +31,7 @@ import org.springframework.data.cassandra.core.query.Columns; import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.repository.Query.Idempotency; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; @@ -229,11 +230,12 @@ class QueryStatementCreator { * @param parameterAccessor must not be {@literal null}. * @return the {@link Statement}. */ - SimpleStatement select(StringBasedQuery stringBasedQuery, CassandraParameterAccessor parameterAccessor) { + SimpleStatement select(StringBasedQuery stringBasedQuery, CassandraParameterAccessor parameterAccessor, + SpELExpressionEvaluator evaluator) { try { - SimpleStatement boundQuery = stringBasedQuery.bindQuery(parameterAccessor, this.queryMethod); + SimpleStatement boundQuery = stringBasedQuery.bindQuery(parameterAccessor, evaluator); Optional queryOptions = Optional.ofNullable(parameterAccessor.getQueryOptions()); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java index a2b142227..d996c82f3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java @@ -49,7 +49,7 @@ import com.datastax.oss.driver.api.core.cql.Statement; @FunctionalInterface interface ReactiveCassandraQueryExecution { - Object execute(Statement statement, Class type); + Publisher execute(Statement statement, Class type); /** * {@link ReactiveCassandraQueryExecution} for a {@link org.springframework.data.domain.Slice}. @@ -72,7 +72,7 @@ interface ReactiveCassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.CassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(Statement statement, Class type) { + public Publisher execute(Statement statement, Class type) { CassandraPageRequest.validatePageable(pageable); @@ -114,7 +114,7 @@ interface ReactiveCassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(Statement statement, Class type) { + public Publisher execute(Statement statement, Class type) { return operations.select(statement, type); } } @@ -139,7 +139,7 @@ interface ReactiveCassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(Statement statement, Class type) { + public Publisher execute(Statement statement, Class type) { return operations.select(statement, type).buffer(2).map(objects -> { @@ -175,7 +175,7 @@ interface ReactiveCassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(com.datastax.oss.driver.api.core.cql.Statement, java.lang.Class) */ @Override - public Object execute(Statement statement, Class type) { + public Publisher execute(Statement statement, Class type) { Mono> rows = this.operations.getReactiveCqlOperations().queryForRows(statement).buffer(2).next(); @@ -223,8 +223,8 @@ interface ReactiveCassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(Statement statement, Class type) { - return converter.convert(delegate.execute(statement, type)); + public Publisher execute(Statement statement, Class type) { + return (Publisher) converter.convert(delegate.execute(statement, type)); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java index 5fbbfa548..4e5eafd74 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java @@ -15,6 +15,8 @@ */ package org.springframework.data.cassandra.repository.query; +import reactor.core.publisher.Mono; + import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.core.StatementFactory; import org.springframework.data.cassandra.core.convert.UpdateMapper; @@ -94,22 +96,25 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor, boolean) */ @Override - protected SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) { + protected Mono createQuery(CassandraParameterAccessor parameterAccessor) { - if (isCountQuery()) { - return getQueryStatementCreator().count(getStatementFactory(), getTree(), parameterAccessor); - } + return Mono.fromSupplier(() -> { - if (isExistsQuery()) { - return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor); - } + if (isCountQuery()) { + return getQueryStatementCreator().count(getStatementFactory(), getTree(), parameterAccessor); + } - if (getTree().isDelete()) { - return getQueryStatementCreator().delete(getStatementFactory(), getTree(), parameterAccessor); - } + if (isExistsQuery()) { + return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor); + } - return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor, - getQueryMethod().getResultProcessor()); + if (getTree().isDelete()) { + return getQueryStatementCreator().delete(getStatementFactory(), getTree(), parameterAccessor); + } + + return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor, + getQueryMethod().getResultProcessor()); + }); } /* (non-Javadoc) 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 3d03d234e..3d5f0b1a0 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 @@ -15,9 +15,15 @@ */ package org.springframework.data.cassandra.repository.query; +import reactor.core.publisher.Mono; + 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.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -45,6 +51,9 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra private final boolean isExistsQuery; + private final ExpressionParser expressionParser; + private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider; + /** * Create a new {@link ReactiveStringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, * {@link ReactiveCassandraOperations}, {@link SpelExpressionParser}, and @@ -59,8 +68,8 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations */ public ReactiveStringBasedCassandraQuery(ReactiveCassandraQueryMethod queryMethod, - ReactiveCassandraOperations operations, SpelExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + ReactiveCassandraOperations operations, ExpressionParser expressionParser, + ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider); } @@ -79,15 +88,17 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations */ public ReactiveStringBasedCassandraQuery(String query, ReactiveCassandraQueryMethod method, - ReactiveCassandraOperations operations, SpelExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + ReactiveCassandraOperations operations, ExpressionParser expressionParser, + ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { super(method, operations); Assert.hasText(query, "Query must not be empty"); - this.stringBasedQuery = new StringBasedQuery(query, - new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider)); + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + + this.stringBasedQuery = new StringBasedQuery(query, method.getParameters(), expressionParser); if (method.hasAnnotatedQuery()) { @@ -113,8 +124,14 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor) */ @Override - public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) { - return getQueryStatementCreator().select(getStringBasedQuery(), parameterAccessor); + public Mono createQuery(CassandraParameterAccessor parameterAccessor) { + + StringBasedQuery query = getStringBasedQuery(); + + Mono spelEvaluator = getSpelEvaluatorFor(query.getExpressionDependencies(), + parameterAccessor); + + return spelEvaluator.map(it -> getQueryStatementCreator().select(query, parameterAccessor, it)); } /* (non-Javadoc) @@ -148,4 +165,22 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra 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, + CassandraParameterAccessor accessor) { + + return evaluationContextProvider + .getEvaluationContextLater(getQueryMethod().getParameters(), accessor.getValues(), dependencies) + .map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(expressionParser, + evaluationContext)) + .defaultIfEmpty(DefaultSpELExpressionEvaluator.unsupported()); + } } 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 e8099ce5f..5ad6eea59 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 @@ -18,6 +18,8 @@ package org.springframework.data.cassandra.repository.query; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.repository.Query; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import com.datastax.oss.driver.api.core.cql.SimpleStatement; @@ -44,6 +46,9 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { private final boolean isExistsQuery; + private final ExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + /** * Create a new {@link StringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, * {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. @@ -57,7 +62,7 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { * @see org.springframework.data.cassandra.core.CassandraOperations */ public StringBasedCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations, - SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider); } @@ -76,12 +81,15 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { * @see org.springframework.data.cassandra.core.CassandraOperations */ public StringBasedCassandraQuery(String query, CassandraQueryMethod method, CassandraOperations operations, - SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { super(method, operations); + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + this.stringBasedQuery = new StringBasedQuery(query, - new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider)); + method.getParameters(), expressionParser); if (method.hasAnnotatedQuery()) { @@ -108,7 +116,14 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { */ @Override public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) { - return getQueryStatementCreator().select(getStringBasedQuery(), parameterAccessor); + + StringBasedQuery query = getStringBasedQuery(); + + EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext( + getQueryMethod().getParameters(), parameterAccessor.getValues(), query.getExpressionDependencies()); + + return getQueryStatementCreator().select(query, parameterAccessor, + new DefaultSpELExpressionEvaluator(expressionParser, evaluationContext)); } /* (non-Javadoc) 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 2457702ff..c1dcf382b 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 @@ -21,8 +21,10 @@ import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.BindingContext; -import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.ParameterBinding; +import org.springframework.data.cassandra.repository.query.BindingContext.ParameterBinding; +import org.springframework.data.mapping.model.SpELExpressionEvaluator; +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; @@ -39,54 +41,76 @@ class StringBasedQuery { private final String query; - private final ExpressionEvaluatingParameterBinder parameterBinder; + private final CassandraParameters parameters; + + private final ExpressionParser expressionParser; private final List queryParameterBindings = new ArrayList<>(); + private final ExpressionDependencies expressionDependencies; + /** - * Create a new {@link StringBasedQuery} given {@code query}, {@link ExpressionEvaluatingParameterBinder} and - * {@link CodecRegistry}. + * Create a new {@link StringBasedQuery} given {@code query}, {@link CassandraParameters} and + * {@link ExpressionParser}. * * @param query must not be empty. - * @param parameterBinder must not be {@literal null}. + * @param parameters must not be {@literal null}. + * @param expressionParser must not be {@literal null}. */ - StringBasedQuery(String query, ExpressionEvaluatingParameterBinder parameterBinder) { - - Assert.hasText(query, "Query must not be empty"); - Assert.notNull(parameterBinder, "ExpressionEvaluatingParameterBinder must not be null"); - - this.parameterBinder = parameterBinder; + StringBasedQuery(String query, CassandraParameters parameters, ExpressionParser expressionParser) { this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, this.queryParameterBindings); - + this.parameters = parameters; + this.expressionParser = expressionParser; + this.expressionDependencies = createExpressionDependencies(); } - private ExpressionEvaluatingParameterBinder getParameterBinder() { - return this.parameterBinder; + private ExpressionDependencies createExpressionDependencies() { + + if (queryParameterBindings.isEmpty()) { + return ExpressionDependencies.none(); + } + + List dependencies = new ArrayList<>(); + + for (ParameterBinding binding : queryParameterBindings) { + if (binding.isExpression()) { + dependencies + .add(ExpressionDependencies.discover(expressionParser.parseExpression(binding.getRequiredExpression()))); + } + } + + return ExpressionDependencies.merged(dependencies); } - /* (non-Javadoc) */ - protected String getQuery() { - return this.query; + /** + * Obtain {@link ExpressionDependencies} from the parsed query. + * + * @return the {@link ExpressionDependencies} from the parsed query. + */ + public ExpressionDependencies getExpressionDependencies() { + return expressionDependencies; } /** * Bind the query to actual parameters using {@link CassandraParameterAccessor}, * * @param parameterAccessor must not be {@literal null}. - * @param queryMethod must not be {@literal null}. + * @param evaluator must not be {@literal null}. * @return the bound String query containing formatted parameters. */ - SimpleStatement bindQuery(CassandraParameterAccessor parameterAccessor, CassandraQueryMethod queryMethod) { + public SimpleStatement bindQuery(CassandraParameterAccessor parameterAccessor, SpELExpressionEvaluator evaluator) { Assert.notNull(parameterAccessor, "CassandraParameterAccessor must not be null"); - Assert.notNull(queryMethod, "CassandraQueryMethod must not be null"); + Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null"); - List arguments = getParameterBinder().bind(parameterAccessor, - new BindingContext(queryMethod, this.queryParameterBindings)); + BindingContext bindingContext = new BindingContext(this.parameters, parameterAccessor, this.queryParameterBindings, + evaluator); - return ParameterBinder.INSTANCE.bind(getQuery(), arguments); + List arguments = bindingContext.getBindingValues(); + + return ParameterBinder.INSTANCE.bind(this.query, arguments); } /** @@ -218,14 +242,15 @@ class StringBasedQuery { result.append(ARGUMENT_PLACEHOLDER); if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) { - bindings.add(ExpressionEvaluatingParameterBinder.ParameterBinding + bindings.add( + BindingContext.ParameterBinding .expression(input.substring(exprStart + 3, currentPosition - 1), true)); } else { if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) { bindings - .add(ExpressionEvaluatingParameterBinder.ParameterBinding.indexed(Integer.parseInt(matcher.group(1)))); + .add(BindingContext.ParameterBinding.indexed(Integer.parseInt(matcher.group(1)))); } else { - bindings.add(ExpressionEvaluatingParameterBinder.ParameterBinding.named(matcher.group(1))); + bindings.add(BindingContext.ParameterBinding.named(matcher.group(1))); } currentPosition = matcher.end(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CachingExpressionParser.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CachingExpressionParser.java new file mode 100644 index 000000000..7e9fe6cd9 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CachingExpressionParser.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.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; + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.ExpressionParser#parseExpression(java.lang.String) + */ + @Override + public Expression parseExpression(String expressionString) throws ParseException { + return cache.computeIfAbsent(expressionString, delegate::parseExpression); + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.ExpressionParser#parseExpression(java.lang.String, org.springframework.expression.ParserContext) + */ + @Override + public Expression parseExpression(String expressionString, ParserContext context) throws ParseException { + throw new UnsupportedOperationException("Parsing using ParserContext is not supported"); + } +} 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 b44d36ddf..eb816b2b2 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 @@ -36,6 +36,7 @@ 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.RepositoryQuery; +import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -112,7 +113,7 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { return Optional.of(new CassandraQueryLookupStrategy(operations, evaluationContextProvider, mappingContext)); } - private class CassandraQueryLookupStrategy implements QueryLookupStrategy { + private static class CassandraQueryLookupStrategy implements QueryLookupStrategy { private final QueryMethodEvaluationContextProvider evaluationContextProvider; @@ -120,6 +121,8 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { private final CassandraOperations operations; + private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER); + CassandraQueryLookupStrategy(CassandraOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider, MappingContext, CassandraPersistentProperty> mappingContext) { @@ -141,13 +144,14 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringBasedCassandraQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER, + return new StringBasedCassandraQuery(namedQuery, queryMethod, operations, expressionParser, evaluationContextProvider); } else if (queryMethod.hasAnnotatedQuery()) { - return new StringBasedCassandraQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider); + return new StringBasedCassandraQuery(queryMethod, operations, expressionParser, evaluationContextProvider); } 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 528ff4f3e..12ebddc50 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 @@ -34,7 +34,9 @@ import org.springframework.data.repository.core.support.ReactiveRepositoryFactor import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -64,6 +66,8 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor this.operations = cassandraOperations; this.mappingContext = cassandraOperations.getConverter().getMappingContext(); + + setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); } /* (non-Javadoc) @@ -91,7 +95,8 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor @Override protected Optional getQueryLookupStrategy(@Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new CassandraQueryLookupStrategy(operations, evaluationContextProvider, mappingContext)); + return Optional.of(new CassandraQueryLookupStrategy(operations, + (ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider, mappingContext)); } /* (non-Javadoc) @@ -113,12 +118,16 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor */ private static class CassandraQueryLookupStrategy implements QueryLookupStrategy { - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider; + private final ReactiveCassandraOperations operations; + private final MappingContext, ? extends CassandraPersistentProperty> mappingContext; + private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER); + CassandraQueryLookupStrategy(ReactiveCassandraOperations operations, - QueryMethodEvaluationContextProvider evaluationContextProvider, + ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, MappingContext, ? extends CassandraPersistentProperty> mappingContext) { this.evaluationContextProvider = evaluationContextProvider; @@ -141,10 +150,10 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new ReactiveStringBasedCassandraQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER, + return new ReactiveStringBasedCassandraQuery(namedQuery, queryMethod, operations, expressionParser, evaluationContextProvider); } else if (queryMethod.hasAnnotatedQuery()) { - return new ReactiveStringBasedCassandraQuery(queryMethod, operations, EXPRESSION_PARSER, + return new ReactiveStringBasedCassandraQuery(queryMethod, operations, expressionParser, evaluationContextProvider); } else { return new ReactivePartTreeCassandraQuery(queryMethod, operations); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactoryBean.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactoryBean.java index 06d0d9bfd..71f76b5dc 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactoryBean.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactoryBean.java @@ -15,11 +15,16 @@ */ package org.springframework.data.cassandra.repository.support; +import java.util.Optional; + +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -80,6 +85,16 @@ public class ReactiveCassandraRepositoryFactoryBean, return getFactoryInstance(operations); } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createDefaultQueryMethodEvaluationContextProvider(ListableBeanFactory) + */ + @Override + protected Optional createDefaultQueryMethodEvaluationContextProvider( + ListableBeanFactory beanFactory) { + return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory)); + } + /** * Creates and initializes a {@link RepositoryFactorySupport} instance. * diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/ReactiveCassandraRepositoryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/ReactiveCassandraRepositoryIntegrationTests.java index acc94eef7..2b72b4128 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/ReactiveCassandraRepositoryIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/ReactiveCassandraRepositoryIntegrationTests.java @@ -111,7 +111,6 @@ public class ReactiveCassandraRepositoryIntegrationTests extends AbstractSpringD factory.setRepositoryBaseClass(SimpleReactiveCassandraRepository.class); factory.setBeanClassLoader(classLoader); factory.setBeanFactory(beanFactory); - factory.setEvaluationContextProvider(QueryMethodEvaluationContextProvider.DEFAULT); repository = factory.getRepository(UserRepository.class); groupRepostitory = factory.getRepository(GroupRepository.class); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ParameterBindingParserUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ParameterBindingParserUnitTests.java index 407346cb7..25c9872d7 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ParameterBindingParserUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ParameterBindingParserUnitTests.java @@ -21,7 +21,7 @@ import java.util.ArrayList; import java.util.List; import org.junit.Test; -import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.ParameterBinding; +import org.springframework.data.cassandra.repository.query.BindingContext.ParameterBinding; import org.springframework.data.cassandra.repository.query.StringBasedQuery.ParameterBindingParser; /** diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java index 64574a836..63d8282fc 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java @@ -176,7 +176,8 @@ public class ReactivePartTreeCassandraQueryUnitTests { CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(partTreeQuery.getQueryMethod(), args); - return partTreeQuery.createQuery(new ConvertingParameterAccessor(mockCassandraOperations.getConverter(), accessor)); + return partTreeQuery.createQuery(new ConvertingParameterAccessor(mockCassandraOperations.getConverter(), accessor)) + .block(); } private ReactivePartTreeCassandraQuery createQueryForMethod(Class repositoryInterface, String methodName, 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 9ed3d6685..01819141c 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 @@ -18,7 +18,10 @@ package org.springframework.data.cassandra.repository.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import reactor.core.publisher.Mono; + import java.lang.reflect.Method; +import java.util.Arrays; import org.junit.Before; import org.junit.Test; @@ -40,8 +43,11 @@ 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.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; +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.util.ReflectionUtils; import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; @@ -79,13 +85,13 @@ public class ReactiveStringBasedCassandraQueryUnitTests { } @Test // DATACASS-335 - public void bindsSimplePropertyCorrectly() throws Exception { + public void bindsSimplePropertyCorrectly() { ReactiveStringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", String.class); CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "White"); - SimpleStatement actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor).block(); assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname=?;"); assertThat(actual.getPositionalValues().get(0)).isEqualTo("White"); @@ -102,7 +108,7 @@ public class ReactiveStringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor parameterAccessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), queryOptions, "White"); - SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor); + SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor).block(); assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname=?;"); assertThat(actual.getPositionalValues().get(0)).isEqualTo("White"); @@ -117,13 +123,27 @@ public class ReactiveStringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor parameterAccessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Matthews"); - SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor); + SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor).block(); assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname=?;"); assertThat(actual.getPositionalValues().get(0)).isEqualTo("Matthews"); assertThat(actual.getConsistencyLevel()).isEqualTo(DefaultConsistencyLevel.LOCAL_ONE); } + @Test // DATACASS-788 + public void shouldUseSpelExtension() { + + ReactiveStringBasedCassandraQuery cassandraQuery = getQueryMethod("findBySpel"); + + 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); @@ -131,8 +151,11 @@ public class ReactiveStringBasedCassandraQueryUnitTests { ReactiveCassandraQueryMethod queryMethod = new ReactiveCassandraQueryMethod(method, metadata, factory, converter.getMappingContext()); + ReactiveExtensionAwareQueryMethodEvaluationContextProvider provider = new ReactiveExtensionAwareQueryMethodEvaluationContextProvider( + Arrays.asList(MyReactiveExtension.INSTANCE, MyDefunctExtension.INSTANCE)); + return new ReactiveStringBasedCassandraQuery(queryMethod, operations, PARSER, - ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT); + provider); } @SuppressWarnings("unused") @@ -145,5 +168,74 @@ public class ReactiveStringBasedCassandraQueryUnitTests { @Query("SELECT * FROM person WHERE lastname=?0;") Person findByLastname(QueryOptions queryOptions, String lastname); + @Query("SELECT * FROM person WHERE lastname=:#{getName()};") + Person findBySpel(); + + } + + public static class MyReactiveExtensionObject implements EvaluationContextExtension { + + public String getName() { + return "Walter"; + } + + @Override + public String getExtensionId() { + return "ext-1"; + } + + @Nullable + @Override + public MyReactiveExtensionObject getRootObject() { + return this; + } + } + + public static class DefunctExtensionObject implements EvaluationContextExtension { + + public String getPrincipal() { + throw new IllegalStateException(); + } + + @Override + public String getExtensionId() { + return "ext-1"; + } + + @Nullable + @Override + public DefunctExtensionObject getRootObject() { + throw new IllegalStateException(); + } + } + + enum MyReactiveExtension implements ReactiveEvaluationContextExtension { + + INSTANCE; + + @Override + public Mono getExtension() { + return Mono.just(new MyReactiveExtensionObject()); + } + + @Override + public String getExtensionId() { + return "ext-1"; + } + } + + enum MyDefunctExtension implements ReactiveEvaluationContextExtension { + + INSTANCE; + + @Override + public Mono getExtension() { + return Mono.error(new IllegalStateException()); + } + + @Override + public String getExtensionId() { + return "ext-2"; + } } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleReactiveCassandraRepositoryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleReactiveCassandraRepositoryIntegrationTests.java index 90776fd40..ff66817d1 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleReactiveCassandraRepositoryIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleReactiveCassandraRepositoryIntegrationTests.java @@ -107,7 +107,6 @@ public class SimpleReactiveCassandraRepositoryIntegrationTests extends AbstractE factory.setRepositoryBaseClass(SimpleReactiveCassandraRepository.class); factory.setBeanClassLoader(classLoader); factory.setBeanFactory(beanFactory); - factory.setEvaluationContextProvider(ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT); repository = factory.getRepository(UserRepostitory.class); diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 5352b7e3f..bee5bbab8 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,6 +7,7 @@ This chapter summarizes changes and new features for each release. == What's new in Spring Data for Apache Cassandra 3.1 * <> enabled through `@EnableReactiveCassandraAuditing`. `@EnableCassandraAuditing` no longer registers `ReactiveAuditingEntityCallback`. +* Reactive SpEL support in `@Query` query methods. [[new-features.3-0-0]] == What's new in Spring Data for Apache Cassandra 3.0