Browse Source

Add `ValueExpression` infrastructure for query methods.

Introduce ValueExpressionQueryRewriter as replacement for SpelQueryContext and QueryMethodValueEvaluationContextAccessor to encapsulate common ValueExpression functionality for Spring Data modules wanting to resolve Value Expressions in query methods.

Reduce dependencies in RepositoryFactoryBeanSupport and RepositoryFactorySupport to EvaluationContextProvider instead of QueryMethodEvaluationContextProvider to simplify dependencies.

Deprecate QueryMethodEvaluationContextProvider and its reactive variant for future removal.

Closes #3049
Original pull request: #3050
pull/3176/head
Mark Paluch 2 years ago
parent
commit
096836ee80
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 6
      src/main/java/org/springframework/data/expression/CompositeValueExpression.java
  2. 3
      src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java
  3. 3
      src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java
  4. 13
      src/main/java/org/springframework/data/expression/ExpressionExpression.java
  5. 5
      src/main/java/org/springframework/data/expression/LiteralValueExpression.java
  6. 7
      src/main/java/org/springframework/data/expression/PlaceholderExpression.java
  7. 54
      src/main/java/org/springframework/data/expression/ReactiveValueEvaluationContextProvider.java
  8. 22
      src/main/java/org/springframework/data/expression/ValueEvaluationContext.java
  9. 7
      src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java
  10. 11
      src/main/java/org/springframework/data/expression/ValueExpression.java
  11. 10
      src/main/java/org/springframework/data/expression/ValueExpressionParser.java
  12. 23
      src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java
  13. 28
      src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java
  14. 68
      src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java
  15. 128
      src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
  16. 66
      src/main/java/org/springframework/data/repository/query/CachingValueExpressionDelegate.java
  17. 86
      src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java
  18. 8
      src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java
  19. 269
      src/main/java/org/springframework/data/repository/query/QueryMethodValueEvaluationContextAccessor.java
  20. 76
      src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java
  21. 2
      src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java
  22. 2
      src/main/java/org/springframework/data/repository/query/SpelEvaluator.java
  23. 2
      src/main/java/org/springframework/data/repository/query/SpelQueryContext.java
  24. 91
      src/main/java/org/springframework/data/repository/query/ValueExpressionDelegate.java
  25. 459
      src/main/java/org/springframework/data/repository/query/ValueExpressionQueryRewriter.java
  26. 5
      src/main/java/org/springframework/data/spel/EvaluationContextProvider.java
  27. 8
      src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java
  28. 5
      src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java
  29. 5
      src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java
  30. 36
      src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java
  31. 160
      src/test/java/org/springframework/data/repository/query/ValueExpressionQueryRewriterUnitTests.java

6
src/main/java/org/springframework/data/expression/CompositeValueExpression.java

@ -72,4 +72,10 @@ record CompositeValueExpression(String raw, List<ValueExpression> expressions) i @@ -72,4 +72,10 @@ record CompositeValueExpression(String raw, List<ValueExpression> expressions) i
return builder.toString();
}
@Override
public Class<?> getValueType(ValueEvaluationContext context) {
return String.class;
}
}

3
src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java

@ -17,6 +17,7 @@ package org.springframework.data.expression; @@ -17,6 +17,7 @@ package org.springframework.data.expression;
import org.springframework.core.env.Environment;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
/**
* Default {@link ValueEvaluationContext}.
@ -24,7 +25,7 @@ import org.springframework.expression.EvaluationContext; @@ -24,7 +25,7 @@ import org.springframework.expression.EvaluationContext;
* @author Mark Paluch
* @since 3.3
*/
record DefaultValueEvaluationContext(Environment environment,
record DefaultValueEvaluationContext(@Nullable Environment environment,
EvaluationContext evaluationContext) implements ValueEvaluationContext {
@Override

3
src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java

@ -22,6 +22,7 @@ import org.springframework.data.spel.ExpressionDependencies; @@ -22,6 +22,7 @@ import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.SystemPropertyUtils;
@ -39,6 +40,8 @@ class DefaultValueExpressionParser implements ValueExpressionParser { @@ -39,6 +40,8 @@ class DefaultValueExpressionParser implements ValueExpressionParser {
public static final int PLACEHOLDER_PREFIX_LENGTH = PLACEHOLDER_PREFIX.length();
public static final char[] QUOTE_CHARS = { '\'', '"' };
public static final ValueExpressionParser DEFAULT = new DefaultValueExpressionParser(SpelExpressionParser::new);
private final ValueParserConfiguration configuration;
public DefaultValueExpressionParser(ValueParserConfiguration configuration) {

13
src/main/java/org/springframework/data/expression/ExpressionExpression.java

@ -47,9 +47,14 @@ record ExpressionExpression(Expression expression, ExpressionDependencies depend @@ -47,9 +47,14 @@ record ExpressionExpression(Expression expression, ExpressionDependencies depend
public Object evaluate(ValueEvaluationContext context) {
EvaluationContext evaluationContext = context.getEvaluationContext();
if (evaluationContext != null) {
return expression.getValue(evaluationContext);
}
return expression.getValue();
return evaluationContext != null ? expression.getValue(evaluationContext) : expression.getValue();
}
@Override
public Class<?> getValueType(ValueEvaluationContext context) {
EvaluationContext evaluationContext = context.getEvaluationContext();
return evaluationContext != null ? expression.getValueType(evaluationContext) : expression.getValueType();
}
}

5
src/main/java/org/springframework/data/expression/LiteralValueExpression.java

@ -39,4 +39,9 @@ record LiteralValueExpression(String expression) implements ValueExpression { @@ -39,4 +39,9 @@ record LiteralValueExpression(String expression) implements ValueExpression {
return expression;
}
@Override
public Class<?> getValueType(ValueEvaluationContext context) {
return String.class;
}
}

7
src/main/java/org/springframework/data/expression/PlaceholderExpression.java

@ -38,7 +38,7 @@ record PlaceholderExpression(String expression) implements ValueExpression { @@ -38,7 +38,7 @@ record PlaceholderExpression(String expression) implements ValueExpression {
}
@Override
public Object evaluate(ValueEvaluationContext context) {
public String evaluate(ValueEvaluationContext context) {
Environment environment = context.getEnvironment();
if (environment != null) {
@ -51,4 +51,9 @@ record PlaceholderExpression(String expression) implements ValueExpression { @@ -51,4 +51,9 @@ record PlaceholderExpression(String expression) implements ValueExpression {
return expression;
}
@Override
public Class<?> getValueType(ValueEvaluationContext context) {
return String.class;
}
}

54
src/main/java/org/springframework/data/expression/ReactiveValueEvaluationContextProvider.java

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
/*
* 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.expression;
import reactor.core.publisher.Mono;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.lang.Nullable;
/**
* Reactive extension to {@link ValueEvaluationContext} for obtaining a {@link ValueEvaluationContext} that participates
* in the reactive flow.
*
* @author Mark Paluch
* @since 3.4
*/
public interface ReactiveValueEvaluationContextProvider extends ValueEvaluationContextProvider {
/**
* Return a {@link ValueEvaluationContext} built using the given parameter values.
*
* @param rootObject the root object to set in the {@link ValueEvaluationContext}.
* @return a mono that emits exactly one {@link ValueEvaluationContext}.
*/
Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject);
/**
* Return a tailored {@link ValueEvaluationContext} built using the given parameter values and considering
* {@link ExpressionDependencies expression dependencies}. The returned {@link ValueEvaluationContext} may contain a
* reduced visibility of methods and properties/fields according to the required {@link ExpressionDependencies
* expression dependencies}.
*
* @param rootObject the root object to set in the {@link ValueEvaluationContext}.
* @param dependencies the requested expression dependencies to be available.
* @return a mono that emits exactly one {@link ValueEvaluationContext}.
*/
default Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return getEvaluationContextLater(rootObject);
}
}

22
src/main/java/org/springframework/data/expression/ValueEvaluationContext.java

@ -36,7 +36,7 @@ public interface ValueEvaluationContext { @@ -36,7 +36,7 @@ public interface ValueEvaluationContext {
* @param evaluationContext
* @return a new {@link ValueEvaluationContext} for the given environment and evaluation context.
*/
static ValueEvaluationContext of(Environment environment, EvaluationContext evaluationContext) {
static ValueEvaluationContext of(@Nullable Environment environment, EvaluationContext evaluationContext) {
return new DefaultValueEvaluationContext(environment, evaluationContext);
}
@ -51,8 +51,26 @@ public interface ValueEvaluationContext { @@ -51,8 +51,26 @@ public interface ValueEvaluationContext {
/**
* Returns the {@link EvaluationContext} if provided.
*
* @return the {@link EvaluationContext} or {@literal null}.
* @return the {@link EvaluationContext} or {@literal null} if not set.
*/
@Nullable
EvaluationContext getEvaluationContext();
/**
* Returns the required {@link EvaluationContext} or throws {@link IllegalStateException} if there is no evaluation
* context available.
*
* @return the {@link EvaluationContext}.
* @since 3.4
*/
default EvaluationContext getRequiredEvaluationContext() {
EvaluationContext evaluationContext = getEvaluationContext();
if (evaluationContext == null) {
throw new IllegalStateException("No evaluation context available");
}
return evaluationContext;
}
}

7
src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java

@ -17,6 +17,7 @@ package org.springframework.data.expression; @@ -17,6 +17,7 @@ package org.springframework.data.expression;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
/**
* SPI to provide to access a centrally defined potentially shared {@link ValueEvaluationContext}.
@ -24,6 +25,7 @@ import org.springframework.expression.EvaluationContext; @@ -24,6 +25,7 @@ import org.springframework.expression.EvaluationContext;
* @author Mark Paluch
* @since 3.3
*/
@FunctionalInterface
public interface ValueEvaluationContextProvider {
/**
@ -32,7 +34,7 @@ public interface ValueEvaluationContextProvider { @@ -32,7 +34,7 @@ public interface ValueEvaluationContextProvider {
* @param rootObject the root object to set in the {@link EvaluationContext}.
* @return
*/
ValueEvaluationContext getEvaluationContext(Object rootObject);
ValueEvaluationContext getEvaluationContext(@Nullable Object rootObject);
/**
* Return a tailored {@link EvaluationContext} built using the given parameter values and considering
@ -44,7 +46,8 @@ public interface ValueEvaluationContextProvider { @@ -44,7 +46,8 @@ public interface ValueEvaluationContextProvider {
* @param dependencies the requested expression dependencies to be available.
* @return
*/
default ValueEvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
default ValueEvaluationContext getEvaluationContext(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return getEvaluationContext(rootObject);
}
}

11
src/main/java/org/springframework/data/expression/ValueExpression.java

@ -62,4 +62,15 @@ public interface ValueExpression { @@ -62,4 +62,15 @@ public interface ValueExpression {
@Nullable
Object evaluate(ValueEvaluationContext context) throws EvaluationException;
/**
* Return the most general type that the expression would use as return type for the given context.
*
* @param context the context in which to evaluate the expression.
* @return the most general type of value.
* @throws EvaluationException if there is a problem determining the type
* @since 3.4
*/
@Nullable
Class<?> getValueType(ValueEvaluationContext context) throws EvaluationException;
}

10
src/main/java/org/springframework/data/expression/ValueExpressionParser.java

@ -27,6 +27,16 @@ import org.springframework.expression.ParseException; @@ -27,6 +27,16 @@ import org.springframework.expression.ParseException;
*/
public interface ValueExpressionParser {
/**
* Creates a default parser to parse expression strings.
*
* @return the parser instance.
* @since 3.4
*/
static ValueExpressionParser create() {
return DefaultValueExpressionParser.DEFAULT;
}
/**
* Creates a new parser to parse expression strings.
*

23
src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java

@ -23,6 +23,7 @@ import org.springframework.data.expression.ValueExpressionParser; @@ -23,6 +23,7 @@ import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.spel.EvaluationContextProvider;
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.ConcurrentLruCache;
@ -38,11 +39,29 @@ public class CachingValueExpressionEvaluatorFactory implements ValueEvaluationCo @@ -38,11 +39,29 @@ public class CachingValueExpressionEvaluatorFactory implements ValueEvaluationCo
private final EnvironmentCapable environmentProvider;
private final EvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link CachingValueExpressionEvaluatorFactory} for the given {@link ExpressionParser},
* {@link EnvironmentCapable Environment provider} and {@link EvaluationContextProvider} with a cache size of 256.
*
* @param expressionParser
* @param environmentProvider
* @param evaluationContextProvider
*/
public CachingValueExpressionEvaluatorFactory(ExpressionParser expressionParser,
EnvironmentCapable environmentProvider, EvaluationContextProvider evaluationContextProvider) {
this(expressionParser, environmentProvider, evaluationContextProvider, 256);
}
/**
* Creates a new {@link CachingValueExpressionEvaluatorFactory} for the given {@link ExpressionParser},
* {@link EnvironmentCapable Environment provider} and {@link EvaluationContextProvider} with a specific
* {@code cacheSize}.
*
* @param expressionParser
* @param environmentProvider
* @param evaluationContextProvider
* @param cacheSize
*/
public CachingValueExpressionEvaluatorFactory(ExpressionParser expressionParser,
EnvironmentCapable environmentProvider, EvaluationContextProvider evaluationContextProvider, int cacheSize) {
@ -55,13 +74,13 @@ public class CachingValueExpressionEvaluatorFactory implements ValueEvaluationCo @@ -55,13 +74,13 @@ public class CachingValueExpressionEvaluatorFactory implements ValueEvaluationCo
}
@Override
public ValueEvaluationContext getEvaluationContext(Object rootObject) {
public ValueEvaluationContext getEvaluationContext(@Nullable Object rootObject) {
return ValueEvaluationContext.of(environmentProvider.getEnvironment(),
evaluationContextProvider.getEvaluationContext(rootObject));
}
@Override
public ValueEvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
public ValueEvaluationContext getEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) {
return ValueEvaluationContext.of(environmentProvider.getEnvironment(),
evaluationContextProvider.getEvaluationContext(rootObject, dependencies));
}

28
src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java

@ -17,14 +17,20 @@ package org.springframework.data.repository.core.support; @@ -17,14 +17,20 @@ package org.springframework.data.repository.core.support;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
import org.reactivestreams.Publisher;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.util.ReactiveWrappers;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
@ -69,6 +75,28 @@ public abstract class ReactiveRepositoryFactorySupport extends RepositoryFactory @@ -69,6 +75,28 @@ public abstract class ReactiveRepositoryFactorySupport extends RepositoryFactory
: evaluationContextProvider);
}
/**
* Returns the {@link QueryLookupStrategy} for the given {@link QueryLookupStrategy.Key} and
* {@link ValueExpressionDelegate}. Favor implementing this method over
* {@link #getQueryLookupStrategy(QueryLookupStrategy.Key, QueryMethodEvaluationContextProvider)} for extended
* {@link org.springframework.data.expression.ValueExpression} support.
* <p>
* This method delegates to
* {@link #getQueryLookupStrategy(QueryLookupStrategy.Key, QueryMethodEvaluationContextProvider)} unless overridden.
* </p>
*
* @param key can be {@literal null}.
* @param valueExpressionDelegate will never be {@literal null}.
* @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up.
* @since 3.4
*/
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable QueryLookupStrategy.Key key,
ValueExpressionDelegate valueExpressionDelegate) {
return getQueryLookupStrategy(key,
new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(getEvaluationContextProvider()));
}
/**
* We need to make sure that the necessary conversion libraries are in place if the repository interface uses RxJava 1
* types.

68
src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java

@ -28,6 +28,8 @@ import org.springframework.beans.factory.InitializingBean; @@ -28,6 +28,8 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.Repository;
@ -41,6 +43,8 @@ import org.springframework.data.repository.query.QueryLookupStrategy; @@ -41,6 +43,8 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.util.Lazy;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
@ -63,8 +67,8 @@ import org.springframework.util.Assert; @@ -63,8 +67,8 @@ import org.springframework.util.Assert;
* @author Johannes Englmeier
*/
public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, S, ID>
implements InitializingBean, RepositoryFactoryInformation<S, ID>, FactoryBean<T>, BeanClassLoaderAware,
BeanFactoryAware, ApplicationEventPublisherAware {
implements InitializingBean, RepositoryFactoryInformation<S, ID>, FactoryBean<T>, ApplicationEventPublisherAware,
BeanClassLoaderAware, BeanFactoryAware, EnvironmentAware {
private final Class<? extends T> repositoryInterface;
@ -74,14 +78,15 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, @@ -74,14 +78,15 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
private Optional<Class<?>> repositoryBaseClass = Optional.empty();
private Optional<Object> customImplementation = Optional.empty();
private Optional<RepositoryFragments> repositoryFragments = Optional.empty();
private NamedQueries namedQueries;
private NamedQueries namedQueries = PropertiesBasedNamedQueries.EMPTY;
private Optional<MappingContext<?, ?>> mappingContext = Optional.empty();
private ClassLoader classLoader;
private ApplicationEventPublisher publisher;
private BeanFactory beanFactory;
private Environment environment;
private boolean lazyInit = false;
private Optional<QueryMethodEvaluationContextProvider> evaluationContextProvider = Optional.empty();
private List<RepositoryFactoryCustomizer> repositoryFactoryCustomizers = new ArrayList<>();
private ApplicationEventPublisher publisher;
private Optional<EvaluationContextProvider> evaluationContextProvider = Optional.empty();
private final List<RepositoryFactoryCustomizer> repositoryFactoryCustomizers = new ArrayList<>();
private Lazy<T> repository;
@ -168,14 +173,26 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, @@ -168,14 +173,26 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.mappingContext = Optional.of(mappingContext);
}
/**
* Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries.
*
* @param evaluationContextProvider must not be {@literal null}.
* @since 3.4
*/
public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = Optional.of(evaluationContextProvider);
}
/**
* Sets the {@link QueryMethodEvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined
* queries.
*
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated since 3.4, use {@link #setEvaluationContextProvider(EvaluationContextProvider)} instead.
*/
@Deprecated(since = "3.4")
public void setEvaluationContextProvider(QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = Optional.of(evaluationContextProvider);
setEvaluationContextProvider(evaluationContextProvider.getEvaluationContextProvider());
}
/**
@ -210,19 +227,38 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, @@ -210,19 +227,38 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.beanFactory = beanFactory;
if (!this.evaluationContextProvider.isPresent() && beanFactory instanceof ListableBeanFactory) {
this.evaluationContextProvider = createDefaultQueryMethodEvaluationContextProvider(
(ListableBeanFactory) beanFactory);
if (this.evaluationContextProvider.isEmpty() && beanFactory instanceof ListableBeanFactory lbf) {
this.evaluationContextProvider = createDefaultEvaluationContextProvider(lbf);
}
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
/**
* Create a default {@link EvaluationContextProvider} (or subclass) from {@link ListableBeanFactory}.
*
* @param beanFactory the bean factory to use.
* @return the default instance. May be {@link Optional#empty()}.
* @since 3.4
*/
protected Optional<EvaluationContextProvider> createDefaultEvaluationContextProvider(
ListableBeanFactory beanFactory) {
return createDefaultQueryMethodEvaluationContextProvider(beanFactory)
.map(QueryMethodEvaluationContextProvider::getEvaluationContextProvider);
}
/**
* Create a default {@link QueryMethodEvaluationContextProvider} (or subclass) from {@link ListableBeanFactory}.
*
* @param beanFactory the bean factory to use.
* @return the default instance. May be {@link Optional#empty()}.
* @since 2.4
* @deprecated since 3.4, use {@link #createDefaultEvaluationContextProvider(ListableBeanFactory)} instead.
*/
@Deprecated(since = "3.4")
protected Optional<QueryMethodEvaluationContextProvider> createDefaultQueryMethodEvaluationContextProvider(
ListableBeanFactory beanFactory) {
return Optional.of(new ExtensionAwareQueryMethodEvaluationContextProvider(beanFactory));
@ -260,20 +296,24 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, @@ -260,20 +296,24 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
return factory.getQueryMethods();
}
@Override
@NonNull
public T getObject() {
return this.repository.get();
}
@Override
@NonNull
public Class<? extends T> getObjectType() {
return repositoryInterface;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void afterPropertiesSet() {
this.factory = createRepositoryFactory();
@ -281,14 +321,18 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, @@ -281,14 +321,18 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey);
this.factory.setNamedQueries(namedQueries);
this.factory.setEvaluationContextProvider(
evaluationContextProvider.orElseGet(() -> QueryMethodEvaluationContextProvider.DEFAULT));
evaluationContextProvider.orElse(QueryMethodValueEvaluationContextAccessor.DEFAULT_CONTEXT_PROVIDER));
this.factory.setBeanClassLoader(classLoader);
this.factory.setBeanFactory(beanFactory);
if (publisher != null) {
if (this.publisher != null) {
this.factory.addRepositoryProxyPostProcessor(new EventPublishingRepositoryProxyPostProcessor(publisher));
}
if (this.environment != null) {
this.factory.setEnvironment(this.environment);
}
repositoryBaseClass.ifPresent(this.factory::setRepositoryBaseClass);
this.repositoryFactoryCustomizers.forEach(customizer -> customizer.customize(this.factory));

128
src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java

@ -38,11 +38,16 @@ import org.springframework.beans.factory.BeanClassLoaderAware; @@ -38,11 +38,16 @@ import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.log.LogMessage;
import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.core.metrics.StartupStep;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@ -54,14 +59,20 @@ import org.springframework.data.repository.core.RepositoryMetadata; @@ -54,14 +59,20 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster;
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster;
import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.transaction.interceptor.TransactionalProxy;
import org.springframework.util.Assert;
@ -80,9 +91,13 @@ import org.springframework.util.ObjectUtils; @@ -80,9 +91,13 @@ import org.springframework.util.ObjectUtils;
* @author John Blum
* @author Johannes Englmeier
*/
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware {
public abstract class RepositoryFactorySupport
implements BeanClassLoaderAware, BeanFactoryAware, EnvironmentAware, EnvironmentCapable {
static final GenericConversionService CONVERSION_SERVICE = new DefaultConversionService();
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final ValueExpressionParser VALUE_PARSER = ValueExpressionParser.create(() -> EXPRESSION_PARSER);
private static final Log logger = LogFactory.getLog(RepositoryFactorySupport.class);
static {
@ -93,15 +108,16 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -93,15 +108,16 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
private final Map<RepositoryInformationCacheKey, RepositoryInformation> repositoryInformationCache;
private final List<RepositoryProxyPostProcessor> postProcessors;
private Optional<Class<?>> repositoryBaseClass;
private @Nullable Class<?> repositoryBaseClass;
private boolean exposeMetadata;
private @Nullable QueryLookupStrategy.Key queryLookupStrategyKey;
private List<QueryCreationListener<?>> queryPostProcessors;
private List<RepositoryMethodInvocationListener> methodInvocationListeners;
private final List<QueryCreationListener<?>> queryPostProcessors;
private final List<RepositoryMethodInvocationListener> methodInvocationListeners;
private NamedQueries namedQueries;
private ClassLoader classLoader;
private QueryMethodEvaluationContextProvider evaluationContextProvider;
private EvaluationContextProvider evaluationContextProvider;
private BeanFactory beanFactory;
private Environment environment;
private Lazy<ProjectionFactory> projectionFactory;
private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener();
@ -112,16 +128,19 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -112,16 +128,19 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
this.repositoryInformationCache = new HashMap<>(16);
this.postProcessors = new ArrayList<>();
this.repositoryBaseClass = Optional.empty();
this.namedQueries = PropertiesBasedNamedQueries.EMPTY;
this.classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
this.evaluationContextProvider = QueryMethodEvaluationContextProvider.DEFAULT;
this.evaluationContextProvider = QueryMethodValueEvaluationContextAccessor.DEFAULT_CONTEXT_PROVIDER;
this.queryPostProcessors = new ArrayList<>();
this.queryPostProcessors.add(collectingListener);
this.methodInvocationListeners = new ArrayList<>();
this.projectionFactory = createProjectionFactory();
}
EvaluationContextProvider getEvaluationContextProvider() {
return evaluationContextProvider;
}
/**
* Set whether the repository method metadata should be exposed by the repository factory as a ThreadLocal for
* retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain
@ -143,7 +162,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -143,7 +162,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
}
/**
* Sets the strategy of how to lookup a query to execute finders.
* Sets the strategy of how to look up a query to execute finders.
*
* @param key
*/
@ -156,12 +175,12 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -156,12 +175,12 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
*
* @param namedQueries the namedQueries to set
*/
public void setNamedQueries(NamedQueries namedQueries) {
public void setNamedQueries(@Nullable NamedQueries namedQueries) {
this.namedQueries = namedQueries == null ? PropertiesBasedNamedQueries.EMPTY : namedQueries;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
public void setBeanClassLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : classLoader;
this.projectionFactory = createProjectionFactory();
}
@ -172,15 +191,42 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -172,15 +191,42 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
this.projectionFactory = createProjectionFactory();
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public Environment getEnvironment() {
if (this.environment == null) {
this.environment = new StandardEnvironment();
}
return this.environment;
}
/**
* Sets the {@link QueryMethodEvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined
* queries.
*
* @param evaluationContextProvider can be {@literal null}, defaults to
* {@link QueryMethodEvaluationContextProvider#DEFAULT}.
* @deprecated since 3.4, use {@link #setEvaluationContextProvider(EvaluationContextProvider)} instead.
*/
public void setEvaluationContextProvider(QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = evaluationContextProvider == null ? QueryMethodEvaluationContextProvider.DEFAULT
@Deprecated(since = "3.4")
public void setEvaluationContextProvider(@Nullable QueryMethodEvaluationContextProvider evaluationContextProvider) {
setEvaluationContextProvider(evaluationContextProvider == null ? EvaluationContextProvider.DEFAULT
: evaluationContextProvider.getEvaluationContextProvider());
}
/**
* Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries.
*
* @param evaluationContextProvider can be {@literal null}, defaults to {@link EvaluationContextProvider#DEFAULT}.
*/
public void setEvaluationContextProvider(@Nullable EvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = evaluationContextProvider == null ? EvaluationContextProvider.DEFAULT
: evaluationContextProvider;
}
@ -191,15 +237,15 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -191,15 +237,15 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
* @param repositoryBaseClass the repository base class to back the repository proxy, can be {@literal null}.
* @since 1.11
*/
public void setRepositoryBaseClass(Class<?> repositoryBaseClass) {
this.repositoryBaseClass = Optional.ofNullable(repositoryBaseClass);
public void setRepositoryBaseClass(@Nullable Class<?> repositoryBaseClass) {
this.repositoryBaseClass = repositoryBaseClass;
}
/**
* Adds a {@link QueryCreationListener} to the factory to plug in functionality triggered right after creation of
* {@link RepositoryQuery} instances.
*
* @param listener
* @param listener the listener to add.
*/
public void addQueryCreationListener(QueryCreationListener<?> listener) {
@ -211,7 +257,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -211,7 +257,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
* Adds a {@link RepositoryMethodInvocationListener} to the factory to plug in functionality triggered right after
* running {@link RepositoryQuery query methods} and {@link Method fragment methods}.
*
* @param listener
* @param listener the listener to add.
* @since 2.4
*/
public void addInvocationListener(RepositoryMethodInvocationListener listener) {
@ -225,7 +271,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -225,7 +271,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
* the proxy gets created. Note that the {@link QueryExecutorMethodInterceptor} will be added to the proxy
* <em>after</em> the {@link RepositoryProxyPostProcessor}s are considered.
*
* @param processor
* @param processor the post-processor to add.
*/
public void addRepositoryProxyPostProcessor(RepositoryProxyPostProcessor processor) {
@ -236,8 +282,8 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -236,8 +282,8 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
/**
* Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add repository-specific extensions.
*
* @param metadata
* @return
* @param metadata the repository metadata to use.
* @return fragment composition.
*/
protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
return RepositoryFragments.empty();
@ -246,8 +292,8 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -246,8 +292,8 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
/**
* Creates {@link RepositoryComposition} based on {@link RepositoryMetadata} for repository-specific method handling.
*
* @param metadata
* @return
* @param metadata the repository metadata to use.
* @return repository composition.
*/
private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata) {
return RepositoryComposition.fromMetadata(metadata);
@ -257,7 +303,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -257,7 +303,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
* Returns a repository instance for the given interface.
*
* @param repositoryInterface must not be {@literal null}.
* @return
* @return the implemented repository interface.
*/
public <T> T getRepository(Class<T> repositoryInterface) {
return getRepository(repositoryInterface, RepositoryFragments.empty());
@ -269,7 +315,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -269,7 +315,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
*
* @param repositoryInterface must not be {@literal null}.
* @param customImplementation must not be {@literal null}.
* @return
* @return the implemented repository interface.
*/
public <T> T getRepository(Class<T> repositoryInterface, Object customImplementation) {
return getRepository(repositoryInterface, RepositoryFragments.just(customImplementation));
@ -281,7 +327,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -281,7 +327,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
*
* @param repositoryInterface must not be {@literal null}.
* @param fragments must not be {@literal null}.
* @return
* @return the implemented repository interface.
* @since 2.0
*/
@SuppressWarnings({ "unchecked" })
@ -298,7 +344,9 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -298,7 +344,9 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
StartupStep repositoryInit = onEvent(applicationStartup, "spring.data.repository.init", repositoryInterface);
repositoryBaseClass.ifPresent(it -> repositoryInit.tag("baseClass", it.getName()));
if (repositoryBaseClass != null) {
repositoryInit.tag("baseClass", repositoryBaseClass.getName());
}
StartupStep repositoryMetadataStep = onEvent(applicationStartup, "spring.data.repository.metadata",
repositoryInterface);
@ -384,7 +432,9 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -384,7 +432,9 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
}
Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
evaluationContextProvider);
new ValueExpressionDelegate(
new QueryMethodValueEvaluationContextAccessor(getEnvironment(), evaluationContextProvider),
VALUE_PARSER));
result.addAdvice(new QueryExecutorMethodInterceptor(information, getProjectionFactory(), queryLookupStrategy,
namedQueries, queryPostProcessors, methodInvocationListeners));
@ -412,7 +462,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -412,7 +462,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
*/
protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) {
SpelAwareProxyProjectionFactory factory = new SpelAwareProxyProjectionFactory();
SpelAwareProxyProjectionFactory factory = new SpelAwareProxyProjectionFactory(EXPRESSION_PARSER);
factory.setBeanClassLoader(classLoader);
factory.setBeanFactory(beanFactory);
@ -476,7 +526,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -476,7 +526,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {
Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));
Class<?> baseClass = repositoryBaseClass != null ? repositoryBaseClass : getRepositoryBaseClass(metadata);
return new DefaultRepositoryInformation(metadata, baseClass, composition);
});
@ -531,12 +581,34 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -531,12 +581,34 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
* @param evaluationContextProvider will never be {@literal null}.
* @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up.
* @since 1.9
* @deprecated since 3.4, use {@link #getQueryLookupStrategy(Key, ValueExpressionDelegate)} instead to support
* {@link org.springframework.data.expression.ValueExpression} in query methods.
*/
@Deprecated(since = "3.4")
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.empty();
}
/**
* Returns the {@link QueryLookupStrategy} for the given {@link Key} and {@link ValueExpressionDelegate}. Favor
* implementing this method over {@link #getQueryLookupStrategy(Key, QueryMethodEvaluationContextProvider)} for
* extended {@link org.springframework.data.expression.ValueExpression} support.
* <p>
* This method delegates to {@link #getQueryLookupStrategy(Key, QueryMethodEvaluationContextProvider)} unless
* overridden.
*
* @param key can be {@literal null}.
* @param valueExpressionDelegate will never be {@literal null}.
* @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up.
* @since 3.4
*/
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
ValueExpressionDelegate valueExpressionDelegate) {
return getQueryLookupStrategy(key,
new ExtensionAwareQueryMethodEvaluationContextProvider(evaluationContextProvider));
}
/**
* Validates the given repository interface as well as the given custom implementation.
*

66
src/main/java/org/springframework/data/repository/query/CachingValueExpressionDelegate.java

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* 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.repository.query;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.util.ConcurrentLruCache;
/**
* Caching variant of {@link ValueExpressionDelegate}.
*
* @author Mark Paluch
* @since 3.4
*/
public class CachingValueExpressionDelegate extends ValueExpressionDelegate {
private final ConcurrentLruCache<String, ValueExpression> expressionCache;
/**
* Creates a new {@link CachingValueExpressionDelegate} given {@link ValueExpressionDelegate}.
*
* @param delegate must not be {@literal null}.
*/
public CachingValueExpressionDelegate(ValueExpressionDelegate delegate) {
super(delegate);
this.expressionCache = new ConcurrentLruCache<>(256, delegate.getValueExpressionParser()::parse);
}
/**
* Creates a new {@link CachingValueExpressionDelegate} given {@link QueryMethodValueEvaluationContextAccessor} and
* {@link ValueExpressionParser}.
*
* @param providerFactory the factory to create value evaluation context providers, must not be {@code null}.
* @param valueExpressionParser the parser to parse expression strings into value expressions, must not be
* {@code null}.
*/
public CachingValueExpressionDelegate(QueryMethodValueEvaluationContextAccessor providerFactory,
ValueExpressionParser valueExpressionParser) {
super(providerFactory, valueExpressionParser);
this.expressionCache = new ConcurrentLruCache<>(256, valueExpressionParser::parse);
}
@Override
public ValueExpressionParser getValueExpressionParser() {
return this;
}
@Override
public ValueExpression parse(String expressionString) throws ParseException {
return expressionCache.get(expressionString);
}
}

86
src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java

@ -15,18 +15,16 @@ @@ -15,18 +15,16 @@
*/
package org.springframework.data.repository.query;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* An {@link QueryMethodEvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of
@ -38,10 +36,26 @@ import org.springframework.util.StringUtils; @@ -38,10 +36,26 @@ import org.springframework.util.StringUtils;
* @author Jens Schauder
* @author Johannes Englmeier
* @since 1.9
* @deprecated since 3.4 in favor of {@link QueryMethodValueEvaluationContextAccessor}.
*/
@Deprecated(since = "3.4")
public class ExtensionAwareQueryMethodEvaluationContextProvider implements QueryMethodEvaluationContextProvider {
private final ExtensionAwareEvaluationContextProvider delegate;
private final QueryMethodValueEvaluationContextAccessor delegate;
/**
* Creates a new {@link ExtensionAwareQueryMethodEvaluationContextProvider}.
*
* @param evaluationContextProvider to lookup the {@link EvaluationContextExtension}s from, must not be
* {@literal null}.
*/
public ExtensionAwareQueryMethodEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) {
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null");
this.delegate = new QueryMethodValueEvaluationContextAccessor(QueryMethodValueEvaluationContextAccessor.ENVIRONMENT,
evaluationContextProvider);
}
/**
* Creates a new {@link ExtensionAwareQueryMethodEvaluationContextProvider}.
@ -53,7 +67,9 @@ public class ExtensionAwareQueryMethodEvaluationContextProvider implements Query @@ -53,7 +67,9 @@ public class ExtensionAwareQueryMethodEvaluationContextProvider implements Query
Assert.notNull(beanFactory, "ListableBeanFactory must not be null");
this.delegate = new ExtensionAwareEvaluationContextProvider(beanFactory);
this.delegate = beanFactory instanceof ApplicationContext ctx ? new QueryMethodValueEvaluationContextAccessor(ctx)
: new QueryMethodValueEvaluationContextAccessor(QueryMethodValueEvaluationContextAccessor.ENVIRONMENT,
beanFactory);
}
/**
@ -66,55 +82,39 @@ public class ExtensionAwareQueryMethodEvaluationContextProvider implements Query @@ -66,55 +82,39 @@ public class ExtensionAwareQueryMethodEvaluationContextProvider implements Query
Assert.notNull(extensions, "EvaluationContextExtensions must not be null");
this.delegate = new org.springframework.data.spel.ExtensionAwareEvaluationContextProvider(extensions);
this.delegate = new QueryMethodValueEvaluationContextAccessor(QueryMethodValueEvaluationContextAccessor.ENVIRONMENT,
extensions);
}
@Override
public <T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues) {
StandardEvaluationContext evaluationContext = delegate.getEvaluationContext(parameterValues);
ExtensionAwareQueryMethodEvaluationContextProvider(QueryMethodValueEvaluationContextAccessor delegate) {
this.delegate = delegate;
}
evaluationContext.setVariables(collectVariables(parameters, parameterValues));
@Override
public EvaluationContextProvider getEvaluationContextProvider() {
return getDelegate().getEvaluationContextProvider();
}
return evaluationContext;
public QueryMethodValueEvaluationContextAccessor getDelegate() {
return delegate;
}
@Override
public <T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues,
ExpressionDependencies dependencies) {
StandardEvaluationContext evaluationContext = delegate.getEvaluationContext(parameterValues, dependencies);
public <T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues) {
evaluationContext.setVariables(collectVariables(parameters, parameterValues));
ValueEvaluationContext evaluationContext = delegate.create(parameters).getEvaluationContext(parameterValues);
return evaluationContext;
return evaluationContext.getRequiredEvaluationContext();
}
/**
* Exposes variables for all named parameters for the given arguments. Also exposes non-bindable parameters under the
* names of their types.
*
* @param parameters must not be {@literal null}.
* @param arguments must not be {@literal null}.
* @return
*/
static Map<String, Object> collectVariables(Parameters<?, ?> parameters, Object[] arguments) {
Map<String, Object> variables = new HashMap<>(parameters.getNumberOfParameters(), 1.0f);
parameters.stream()//
.filter(Parameter::isSpecialParameter)//
.forEach(it -> variables.put(//
StringUtils.uncapitalize(it.getType().getSimpleName()), //
arguments[it.getIndex()]));
@Override
public <T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues,
ExpressionDependencies dependencies) {
parameters.stream()//
.filter(Parameter::isNamedParameter)//
.forEach(it -> variables.put(//
it.getName().orElseThrow(() -> new IllegalStateException("Should never occur")), //
arguments[it.getIndex()]));
ValueEvaluationContext evaluationContext = delegate.create(parameters).getEvaluationContext(parameterValues,
dependencies);
return variables;
return evaluationContext.getRequiredEvaluationContext();
}
}

8
src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java

@ -17,6 +17,7 @@ package org.springframework.data.repository.query; @@ -17,6 +17,7 @@ package org.springframework.data.repository.query;
import java.util.Collections;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
@ -27,7 +28,9 @@ import org.springframework.expression.EvaluationContext; @@ -27,7 +28,9 @@ import org.springframework.expression.EvaluationContext;
* @author Oliver Gierke
* @author Christoph Strobl
* @since 1.9
* @deprecated since 3.4 in favor of {@link QueryMethodValueEvaluationContextAccessor}.
*/
@Deprecated(since = "3.4")
public interface QueryMethodEvaluationContextProvider {
QueryMethodEvaluationContextProvider DEFAULT = new ExtensionAwareQueryMethodEvaluationContextProvider(
@ -51,4 +54,9 @@ public interface QueryMethodEvaluationContextProvider { @@ -51,4 +54,9 @@ public interface QueryMethodEvaluationContextProvider {
*/
<T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues,
ExpressionDependencies dependencies);
/**
* @return the underlying {@link EvaluationContextProvider}.
*/
EvaluationContextProvider getEvaluationContextProvider();
}

269
src/main/java/org/springframework/data/repository/query/QueryMethodValueEvaluationContextAccessor.java

@ -0,0 +1,269 @@ @@ -0,0 +1,269 @@
/*
* 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.repository.query;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ReactiveValueEvaluationContextProvider;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.ReactiveEvaluationContextProvider;
import org.springframework.data.spel.ReactiveExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.ExtensionIdAware;
import org.springframework.data.util.ReactiveWrappers;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Factory to create {@link ValueEvaluationContextProvider} instances. Supports its reactive variant
* {@link ReactiveValueEvaluationContextProvider} if the underlying {@link EvaluationContextProvider} is a reactive one.
*
* @author Mark Paluch
* @since 3.4
*/
public class QueryMethodValueEvaluationContextAccessor {
public static final EvaluationContextProvider DEFAULT_CONTEXT_PROVIDER = createEvaluationContextProvider(
Collections.emptyList());
static final StandardEnvironment ENVIRONMENT = new StandardEnvironment();
private final @Nullable Environment environment;
private final EvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link QueryMethodValueEvaluationContextAccessor} from {@link ApplicationContext}.
*
* @param context the application context to use, must not be {@literal null}.
*/
public QueryMethodValueEvaluationContextAccessor(ApplicationContext context) {
Assert.notNull(context, "ApplicationContext must not be null");
this.environment = context.getEnvironment();
this.evaluationContextProvider = createEvaluationContextProvider(context);
}
/**
* Creates a new {@link QueryMethodValueEvaluationContextAccessor} from {@link Environment} and
* {@link ListableBeanFactory}.
*
* @param environment
* @param beanFactory the bean factory to use, must not be {@literal null}.
*/
public QueryMethodValueEvaluationContextAccessor(@Nullable Environment environment, ListableBeanFactory beanFactory) {
this(environment, createEvaluationContextProvider(beanFactory));
}
/**
* Creates a new {@link QueryMethodValueEvaluationContextAccessor} from {@link Environment} and
* {@link EvaluationContextProvider}.
*
* @param environment
* @param evaluationContextProvider the underlying {@link EvaluationContextProvider} to use, must not be
* {@literal null}.
*/
public QueryMethodValueEvaluationContextAccessor(@Nullable Environment environment,
EvaluationContextProvider evaluationContextProvider) {
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null");
this.environment = environment;
this.evaluationContextProvider = evaluationContextProvider;
}
/**
* Creates a new {@link QueryMethodValueEvaluationContextAccessor} for the given {@link EvaluationContextExtension}s.
*
* @param environment
* @param extensions must not be {@literal null}.
*/
public QueryMethodValueEvaluationContextAccessor(@Nullable Environment environment,
Collection<? extends ExtensionIdAware> extensions) {
Assert.notNull(extensions, "EvaluationContextExtensions must not be null");
this.environment = environment;
this.evaluationContextProvider = createEvaluationContextProvider(extensions);
}
private static EvaluationContextProvider createEvaluationContextProvider(ListableBeanFactory factory) {
return ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)
? new ReactiveExtensionAwareEvaluationContextProvider(factory)
: new ExtensionAwareEvaluationContextProvider(factory);
}
private static EvaluationContextProvider createEvaluationContextProvider(
Collection<? extends ExtensionIdAware> extensions) {
return ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)
? new ReactiveExtensionAwareEvaluationContextProvider(extensions)
: new ExtensionAwareEvaluationContextProvider(extensions);
}
/**
* Creates a default {@link QueryMethodValueEvaluationContextAccessor} using the
* {@link org.springframework.core.env.StandardEnvironment} and extension-less
* {@link org.springframework.data.spel.EvaluationContextProvider}.
*
* @return a default {@link ValueExpressionDelegate}.
*/
public static QueryMethodValueEvaluationContextAccessor create() {
return new QueryMethodValueEvaluationContextAccessor(ENVIRONMENT, DEFAULT_CONTEXT_PROVIDER);
}
EvaluationContextProvider getEvaluationContextProvider() {
return evaluationContextProvider;
}
/**
* Creates a new {@link ValueEvaluationContextProvider} for the given {@link Parameters}.
*
* @param parameters must not be {@literal null}.
* @return a new {@link ValueEvaluationContextProvider} for the given {@link Parameters}.
*/
public ValueEvaluationContextProvider create(Parameters<?, ?> parameters) {
Assert.notNull(parameters, "Parameters must not be null");
if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) {
if (evaluationContextProvider instanceof ReactiveEvaluationContextProvider reactive) {
return new DefaultReactiveQueryMethodValueEvaluationContextProvider(environment, parameters, reactive);
}
}
return new DefaultQueryMethodValueEvaluationContextProvider(environment, parameters, evaluationContextProvider);
}
/**
* Exposes variables for all named parameters for the given arguments. Also exposes non-bindable parameters under the
* names of their types.
*
* @param parameters must not be {@literal null}.
* @param arguments must not be {@literal null}.
* @return
*/
static Map<String, Object> collectVariables(Parameters<?, ?> parameters, Object[] arguments) {
if (parameters.getNumberOfParameters() != arguments.length) {
throw new IllegalArgumentException(
"Number of method parameters (%d) must match the number of method invocation arguments (%d)"
.formatted(parameters.getNumberOfParameters(), arguments.length));
}
Map<String, Object> variables = new HashMap<>(parameters.getNumberOfParameters(), 1.0f);
for (Parameter parameter : parameters) {
if (parameter.isSpecialParameter()) {
variables.put(//
StringUtils.uncapitalize(parameter.getType().getSimpleName()), //
arguments[parameter.getIndex()]);
}
if (parameter.isNamedParameter()) {
variables.put(parameter.getRequiredName(), //
arguments[parameter.getIndex()]);
}
}
return variables;
}
/**
* Imperative {@link ValueEvaluationContextProvider} variant.
*/
static class DefaultQueryMethodValueEvaluationContextProvider implements ValueEvaluationContextProvider {
final @Nullable Environment environment;
final Parameters<?, ?> parameters;
final EvaluationContextProvider delegate;
DefaultQueryMethodValueEvaluationContextProvider(@Nullable Environment environment, Parameters<?, ?> parameters,
EvaluationContextProvider delegate) {
this.environment = environment;
this.parameters = parameters;
this.delegate = delegate;
}
@Override
public ValueEvaluationContext getEvaluationContext(@Nullable Object rootObject) {
return doGetEvaluationContext(delegate.getEvaluationContext(rootObject), rootObject);
}
@Override
public ValueEvaluationContext getEvaluationContext(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return doGetEvaluationContext(delegate.getEvaluationContext(rootObject, dependencies), rootObject);
}
ValueEvaluationContext doGetEvaluationContext(EvaluationContext evaluationContext, @Nullable Object rootObject) {
if (rootObject instanceof Object[] parameterValues) {
collectVariables(parameters, parameterValues).forEach(evaluationContext::setVariable);
}
return ValueEvaluationContext.of(environment, evaluationContext);
}
}
/**
* Reactive {@link ValueEvaluationContextProvider} extension to
* {@link DefaultQueryMethodValueEvaluationContextProvider}.
*/
static class DefaultReactiveQueryMethodValueEvaluationContextProvider
extends DefaultQueryMethodValueEvaluationContextProvider implements ReactiveValueEvaluationContextProvider {
private final ReactiveEvaluationContextProvider delegate;
DefaultReactiveQueryMethodValueEvaluationContextProvider(@Nullable Environment environment,
Parameters<?, ?> parameters, ReactiveEvaluationContextProvider delegate) {
super(environment, parameters, delegate);
this.delegate = delegate;
}
@Override
public Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject) {
return delegate.getEvaluationContextLater(rootObject).map(it -> doGetEvaluationContext(it, rootObject));
}
@Override
public Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return delegate.getEvaluationContextLater(rootObject, dependencies)
.map(it -> doGetEvaluationContext(it, rootObject));
}
}
}

76
src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java

@ -20,13 +20,13 @@ import reactor.core.publisher.Mono; @@ -20,13 +20,13 @@ import reactor.core.publisher.Mono;
import java.util.List;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.data.expression.ReactiveValueEvaluationContextProvider;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.spel.ReactiveExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.ExtensionIdAware;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
/**
* An reactive {@link QueryMethodEvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of
@ -35,12 +35,13 @@ import org.springframework.util.Assert; @@ -35,12 +35,13 @@ import org.springframework.util.Assert;
*
* @author Mark Paluch
* @since 2.4
* @deprecated since 3.4 in favor of {@link QueryMethodValueEvaluationContextAccessor}.
*/
@Deprecated(since = "3.4")
public class ReactiveExtensionAwareQueryMethodEvaluationContextProvider
extends ExtensionAwareQueryMethodEvaluationContextProvider
implements ReactiveQueryMethodEvaluationContextProvider {
private final ReactiveExtensionAwareEvaluationContextProvider delegate;
/**
* Create a new {@link ReactiveExtensionAwareQueryMethodEvaluationContextProvider}.
*
@ -48,10 +49,7 @@ public class ReactiveExtensionAwareQueryMethodEvaluationContextProvider @@ -48,10 +49,7 @@ public class ReactiveExtensionAwareQueryMethodEvaluationContextProvider
* be {@literal null}.
*/
public ReactiveExtensionAwareQueryMethodEvaluationContextProvider(ListableBeanFactory beanFactory) {
Assert.notNull(beanFactory, "ListableBeanFactory must not be null");
this.delegate = new ReactiveExtensionAwareEvaluationContextProvider(beanFactory);
super(beanFactory);
}
/**
@ -62,61 +60,39 @@ public class ReactiveExtensionAwareQueryMethodEvaluationContextProvider @@ -62,61 +60,39 @@ public class ReactiveExtensionAwareQueryMethodEvaluationContextProvider
* @param extensions must not be {@literal null}.
*/
public ReactiveExtensionAwareQueryMethodEvaluationContextProvider(List<? extends ExtensionIdAware> extensions) {
Assert.notNull(extensions, "EvaluationContextExtensions must not be null");
this.delegate = new ReactiveExtensionAwareEvaluationContextProvider(extensions);
}
@Override
public <T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues) {
EvaluationContext evaluationContext = delegate.getEvaluationContext(parameterValues);
if (evaluationContext instanceof StandardEvaluationContext) {
((StandardEvaluationContext) evaluationContext).setVariables(
ExtensionAwareQueryMethodEvaluationContextProvider.collectVariables(parameters, parameterValues));
}
return evaluationContext;
super(new QueryMethodValueEvaluationContextAccessor(QueryMethodValueEvaluationContextAccessor.ENVIRONMENT,
extensions));
}
@Override
public <T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues,
ExpressionDependencies dependencies) {
EvaluationContext evaluationContext = delegate.getEvaluationContext(parameterValues, dependencies);
if (evaluationContext instanceof StandardEvaluationContext) {
((StandardEvaluationContext) evaluationContext).setVariables(
ExtensionAwareQueryMethodEvaluationContextProvider.collectVariables(parameters, parameterValues));
}
return evaluationContext;
/**
* Creates a new {@link ReactiveExtensionAwareQueryMethodEvaluationContextProvider}.
*
* @param evaluationContextProvider to lookup the {@link EvaluationContextExtension}s from, must not be
* {@literal null}.
*/
public ReactiveExtensionAwareQueryMethodEvaluationContextProvider(
EvaluationContextProvider evaluationContextProvider) {
super(new QueryMethodValueEvaluationContextAccessor(QueryMethodValueEvaluationContextAccessor.ENVIRONMENT,
evaluationContextProvider));
}
@Override
public <T extends Parameters<?, ?>> Mono<EvaluationContext> getEvaluationContextLater(T parameters,
Object[] parameterValues) {
Mono<StandardEvaluationContext> evaluationContext = delegate.getEvaluationContextLater(parameterValues);
return evaluationContext
.doOnNext(it -> it.setVariables(
ExtensionAwareQueryMethodEvaluationContextProvider.collectVariables(parameters, parameterValues)))
.cast(EvaluationContext.class);
return createProvider(parameters).getEvaluationContextLater(parameterValues)
.map(ValueEvaluationContext::getRequiredEvaluationContext);
}
@Override
public <T extends Parameters<?, ?>> Mono<EvaluationContext> getEvaluationContextLater(T parameters,
Object[] parameterValues, ExpressionDependencies dependencies) {
Mono<StandardEvaluationContext> evaluationContext = delegate.getEvaluationContextLater(parameterValues,
dependencies);
return createProvider(parameters).getEvaluationContextLater(parameterValues, dependencies)
.map(ValueEvaluationContext::getRequiredEvaluationContext);
}
return evaluationContext
.doOnNext(it -> it.setVariables(
ExtensionAwareQueryMethodEvaluationContextProvider.collectVariables(parameters, parameterValues)))
.cast(EvaluationContext.class);
private ReactiveValueEvaluationContextProvider createProvider(Parameters<?, ?> parameters) {
return (ReactiveValueEvaluationContextProvider) getDelegate().create(parameters);
}
}

2
src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java

@ -28,7 +28,9 @@ import org.springframework.expression.EvaluationContext; @@ -28,7 +28,9 @@ import org.springframework.expression.EvaluationContext;
*
* @author Mark Paluch
* @since 2.4
* @deprecated since 4.0 in favor of {@link QueryMethodValueEvaluationContextAccessor}.
*/
@Deprecated(since = "4.0")
public interface ReactiveQueryMethodEvaluationContextProvider extends QueryMethodEvaluationContextProvider {
ReactiveQueryMethodEvaluationContextProvider DEFAULT = new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(

2
src/main/java/org/springframework/data/repository/query/SpelEvaluator.java

@ -35,7 +35,9 @@ import org.springframework.util.Assert; @@ -35,7 +35,9 @@ import org.springframework.util.Assert;
* @author Oliver Gierke
* @since 2.1
* @see SpelQueryContext#parse(String)
* @deprecated since 3.3, use {@link ValueExpressionQueryRewriter} instead.
*/
@Deprecated(since = "3.3")
public class SpelEvaluator {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();

2
src/main/java/org/springframework/data/repository/query/SpelQueryContext.java

@ -58,7 +58,9 @@ import org.springframework.util.Assert; @@ -58,7 +58,9 @@ import org.springframework.util.Assert;
* @author Gerrit Meier
* @author Mark Paluch
* @since 2.1
* @deprecated since 3.3, use {@link ValueExpressionQueryRewriter} instead.
*/
@Deprecated(since = "3.3")
public class SpelQueryContext {
private static final String SPEL_PATTERN_STRING = "([:?])#\\{([^}]+)}";

91
src/main/java/org/springframework/data/repository/query/ValueExpressionDelegate.java

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
/*
* 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.repository.query;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.expression.ParseException;
/**
* Delegate to provide a {@link ValueExpressionParser} along with a context factory.
* <p>
* Subclasses can customize parsing behavior.
*
* @author Mark Paluch
*/
public class ValueExpressionDelegate implements ValueExpressionParser {
private final QueryMethodValueEvaluationContextAccessor contextAccessor;
private final ValueExpressionParser valueExpressionParser;
/**
* Creates a new {@link ValueExpressionDelegate} given {@link QueryMethodValueEvaluationContextAccessor} and
* {@link ValueExpressionParser}.
*
* @param contextAccessor the factory to create value evaluation context providers, must not be {@code null}.
* @param valueExpressionParser the parser to parse expression strings into value expressions, must not be
* {@code null}.
*/
public ValueExpressionDelegate(QueryMethodValueEvaluationContextAccessor contextAccessor,
ValueExpressionParser valueExpressionParser) {
this.contextAccessor = contextAccessor;
this.valueExpressionParser = valueExpressionParser;
}
ValueExpressionDelegate(ValueExpressionDelegate original) {
this.contextAccessor = original.contextAccessor;
this.valueExpressionParser = original.valueExpressionParser;
}
/**
* Creates a default {@link ValueExpressionDelegate} using the
* {@link org.springframework.core.env.StandardEnvironment}, a default {@link ValueExpression} and extension-less
* {@link org.springframework.data.spel.EvaluationContextProvider}.
*
* @return a default {@link ValueExpressionDelegate}.
*/
public static ValueExpressionDelegate create() {
return new ValueExpressionDelegate(QueryMethodValueEvaluationContextAccessor.create(),
ValueExpressionParser.create());
}
public ValueExpressionParser getValueExpressionParser() {
return valueExpressionParser;
}
public QueryMethodValueEvaluationContextAccessor getEvaluationContextAccessor() {
return contextAccessor;
}
/**
* Creates a {@link ValueEvaluationContextProvider} for query method {@link Parameters} for later creation of a
* {@link ValueEvaluationContext} based on the actual method parameter values. The resulting
* {@link ValueEvaluationContextProvider} is only valid for the given parameters
*
* @param parameters the query method parameters to use.
* @return
*/
public ValueEvaluationContextProvider createValueContextProvider(Parameters<?, ?> parameters) {
return contextAccessor.create(parameters);
}
@Override
public ValueExpression parse(String expressionString) throws ParseException {
return valueExpressionParser.parse(expressionString);
}
}

459
src/main/java/org/springframework/data/repository/query/ValueExpressionQueryRewriter.java

@ -0,0 +1,459 @@ @@ -0,0 +1,459 @@
/*
* Copyright 2018-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.repository.query;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A {@literal ValueExpressionQueryRewriter} is able to detect Value expressions in a query string and to replace them
* with bind variables.
* <p>
* Result of the parse process is a {@link ParsedQuery} which provides the transformed query string. Alternatively and
* preferred one may provide a {@link QueryMethodValueEvaluationContextAccessor} via
* {@link #withEvaluationContextAccessor(QueryMethodValueEvaluationContextAccessor)} which will yield the more powerful
* {@link EvaluatingValueExpressionQueryRewriter}.
* <p>
* Typical usage looks like
*
* <pre class="code">
* ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter
* .of(valueExpressionParser, (counter, expression) -> String.format("__$synthetic$__%d", counter), String::concat)
* .withEvaluationContextAccessor(evaluationContextProviderFactory);
*
* ValueExpressionQueryRewriter.QueryExpressionEvaluator evaluator = rewriter.parse(query, queryMethod.getParameters());
*
* evaluator.evaluate(objects).forEach(parameterMap::addValue);
* </pre>
*
* @author Jens Schauder
* @author Gerrit Meier
* @author Mark Paluch
* @since 3.3
* @see ValueExpression
*/
public class ValueExpressionQueryRewriter {
private static final Pattern EXPRESSION_PATTERN = Pattern.compile("([:?])([#$]\\{[^}]+})");
private final ValueExpressionParser expressionParser;
/**
* A function from the index of a Value expression in a query and the actual Value Expression to the parameter name to
* be used in place of the Value Expression. A typical implementation is expected to look like
* {@code (index, expression) -> "__some_placeholder_" + index}.
*/
private final BiFunction<Integer, String, String> parameterNameSource;
/**
* A function from a prefix used to demarcate a Value Expression in a query and a parameter name as returned from
* {@link #parameterNameSource} to a {@literal String} to be used as a replacement of the Value Expressions in the
* query. The returned value should normally be interpretable as a bind parameter by the underlying persistence
* mechanism. A typical implementation is expected to look like {@code (prefix, name) -> prefix + name} or
* {@code (prefix, name) -> "{" + name + "}"}.
*/
private final BiFunction<String, String, String> replacementSource;
private ValueExpressionQueryRewriter(ValueExpressionParser expressionParser,
BiFunction<Integer, String, String> parameterNameSource, BiFunction<String, String, String> replacementSource) {
Assert.notNull(expressionParser, "ValueExpressionParser must not be null");
Assert.notNull(parameterNameSource, "Parameter name source must not be null");
Assert.notNull(replacementSource, "Replacement source must not be null");
this.parameterNameSource = parameterNameSource;
this.replacementSource = replacementSource;
this.expressionParser = expressionParser;
}
/**
* Creates a new ValueExpressionQueryRewriter using the given {@link ValueExpressionParser} and rewrite functions.
*
* @param expressionParser the expression parser to use.
* @param parameterNameSource function to generate parameter names. Typically, a function of the form
* {@code (index, expression) -> "__some_placeholder_" + index}.
* @param replacementSource function to generate replacements. Typically, a concatenation of the prefix and the
* parameter name such as {@code String::concat}.
* @return a ValueExpressionQueryRewriter instance to rewrite queries and extract parsed {@link ValueExpression}s.
*/
public static ValueExpressionQueryRewriter of(ValueExpressionParser expressionParser,
BiFunction<Integer, String, String> parameterNameSource, BiFunction<String, String, String> replacementSource) {
return new ValueExpressionQueryRewriter(expressionParser, parameterNameSource, replacementSource);
}
/**
* Creates a new EvaluatingValueExpressionQueryRewriter using the given {@link ValueExpressionDelegate} and rewrite
* functions.
*
* @param delegate the ValueExpressionDelegate to use for parsing and to obtain EvaluationContextAccessor from.
* @param parameterNameSource function to generate parameter names. Typically, a function of the form
* {@code (index, expression) -> "__some_placeholder_" + index}.
* @param replacementSource function to generate replacements. Typically, a concatenation of the prefix and the
* parameter name such as {@code String::concat}.
* @return a EvaluatingValueExpressionQueryRewriter instance to rewrite queries and extract parsed
* {@link ValueExpression}s.
* @since 3.4
*/
public static EvaluatingValueExpressionQueryRewriter of(ValueExpressionDelegate delegate,
BiFunction<Integer, String, String> parameterNameSource, BiFunction<String, String, String> replacementSource) {
return of((ValueExpressionParser) delegate, parameterNameSource, replacementSource)
.withEvaluationContextAccessor(delegate.getEvaluationContextAccessor());
}
/**
* Parses the query for {@link org.springframework.data.expression.ValueExpression value expressions} using the
* pattern:
*
* <pre>
* &lt;prefix&gt;#{&lt;spel&gt;}
* &lt;prefix&gt;${&lt;property placeholder&gt;}
* </pre>
* <p>
* with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double
* quotation marks.
*
* @param query a query containing Value Expressions in the format described above. Must not be {@literal null}.
* @return A {@link ParsedQuery} which makes the query with Value Expressions replaced by bind parameters and a map
* from bind parameter to Value Expression available. Guaranteed to be not {@literal null}.
*/
public ParsedQuery parse(String query) {
return new ParsedQuery(expressionParser, query);
}
/**
* Creates a {@link EvaluatingValueExpressionQueryRewriter} from the current one and the given
* {@link QueryMethodValueEvaluationContextAccessor}.
*
* @param accessor must not be {@literal null}.
* @return EvaluatingValueExpressionQueryRewriter instance to rewrite and evaluate Value Expressions.
*/
public EvaluatingValueExpressionQueryRewriter withEvaluationContextAccessor(
QueryMethodValueEvaluationContextAccessor accessor) {
Assert.notNull(accessor, "QueryMethodValueEvaluationContextAccessor must not be null");
return new EvaluatingValueExpressionQueryRewriter(expressionParser, accessor, parameterNameSource,
replacementSource);
}
/**
* An extension of {@link ValueExpressionQueryRewriter} that can create {@link QueryExpressionEvaluator} instances as
* it also knows about a {@link QueryMethodValueEvaluationContextAccessor}.
*
* @author Oliver Gierke
*/
public static class EvaluatingValueExpressionQueryRewriter extends ValueExpressionQueryRewriter {
private final QueryMethodValueEvaluationContextAccessor contextProviderFactory;
/**
* Creates a new {@link EvaluatingValueExpressionQueryRewriter} for the given
* {@link QueryMethodValueEvaluationContextAccessor}, parameter name source and replacement source.
*
* @param factory must not be {@literal null}.
* @param parameterNameSource must not be {@literal null}.
* @param replacementSource must not be {@literal null}.
*/
private EvaluatingValueExpressionQueryRewriter(ValueExpressionParser expressionParser,
QueryMethodValueEvaluationContextAccessor factory,
BiFunction<Integer, String, String> parameterNameSource, BiFunction<String, String, String> replacementSource) {
super(expressionParser, parameterNameSource, replacementSource);
this.contextProviderFactory = factory;
}
/**
* Parses the query for Value Expressions using the pattern:
*
* <pre>
* &lt;prefix&gt;#{&lt;spel&gt;}
* &lt;prefix&gt;${&lt;property placeholder&gt;}
* </pre>
* <p>
* with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double
* quotation marks.
*
* @param query a query containing Value Expressions in the format described above. Must not be {@literal null}.
* @param parameters a {@link Parameters} instance describing query method parameters
* @return A {@link QueryExpressionEvaluator} which allows to evaluate the Value Expressions.
*/
public QueryExpressionEvaluator parse(String query, Parameters<?, ?> parameters) {
return new QueryExpressionEvaluator(contextProviderFactory.create(parameters), parse(query));
}
}
/**
* Parses a query string, identifies the contained Value expressions, replaces them with bind parameters and offers a
* {@link Map} from those bind parameters to the value expression.
* <p>
* The parser detects quoted parts of the query string and does not detect value expressions inside such quoted parts
* of the query.
*
* @author Jens Schauder
* @author Oliver Gierke
* @author Mark Paluch
*/
public class ParsedQuery {
private static final int PREFIX_GROUP_INDEX = 1;
private static final int EXPRESSION_GROUP_INDEX = 2;
private final String query;
private final Map<String, ValueExpression> expressions;
private final QuotationMap quotations;
/**
* Creates a ExpressionDetector from a query String.
*
* @param query must not be {@literal null}.
*/
ParsedQuery(ValueExpressionParser parser, String query) {
Assert.notNull(query, "Query must not be null");
Map<String, ValueExpression> expressions = new HashMap<>();
Matcher matcher = EXPRESSION_PATTERN.matcher(query);
StringBuilder resultQuery = new StringBuilder();
QuotationMap quotedAreas = new QuotationMap(query);
int expressionCounter = 0;
int matchedUntil = 0;
while (matcher.find()) {
if (quotedAreas.isQuoted(matcher.start())) {
resultQuery.append(query, matchedUntil, matcher.end());
} else {
String expressionString = matcher.group(EXPRESSION_GROUP_INDEX);
String prefix = matcher.group(PREFIX_GROUP_INDEX);
String parameterName = parameterNameSource.apply(expressionCounter, expressionString);
String replacement = replacementSource.apply(prefix, parameterName);
resultQuery.append(query, matchedUntil, matcher.start());
resultQuery.append(replacement);
expressions.put(parameterName, parser.parse(expressionString));
expressionCounter++;
}
matchedUntil = matcher.end();
}
resultQuery.append(query.substring(matchedUntil));
this.expressions = Collections.unmodifiableMap(expressions);
this.query = resultQuery.toString();
// recreate quotation map based on rewritten query.
this.quotations = new QuotationMap(this.query);
}
/**
* The query with all the Value Expressions replaced with bind parameters.
*
* @return Guaranteed to be not {@literal null}.
*/
public String getQueryString() {
return query;
}
/**
* Return whether the {@link #getQueryString() query} at {@code index} is quoted.
*
* @param index
* @return {@literal true} if quoted; {@literal false} otherwise.
*/
public boolean isQuoted(int index) {
return quotations.isQuoted(index);
}
public ValueExpression getParameter(String name) {
return expressions.get(name);
}
/**
* Returns the number of expressions in this extractor.
*
* @return the number of expressions in this extractor.
*/
public int size() {
return expressions.size();
}
/**
* Returns whether the query contains Value Expressions.
*
* @return {@literal true} if the query contains Value Expressions.
*/
public boolean hasParameterBindings() {
return !expressions.isEmpty();
}
/**
* A {@literal Map} from parameter name to Value Expression.
*
* @return Guaranteed to be not {@literal null}.
*/
public Map<String, ValueExpression> getParameterMap() {
return expressions;
}
}
/**
* Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers an
* API to query that information.
*
* @author Jens Schauder
* @author Oliver Gierke
* @since 2.1
*/
static class QuotationMap {
private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\'');
private final List<Range<Integer>> quotedRanges = new ArrayList<>();
/**
* Creates a new {@link QuotationMap} for the query.
*
* @param query can be {@literal null}.
*/
public QuotationMap(@Nullable String query) {
if (query == null) {
return;
}
Character inQuotation = null;
int start = 0;
for (int i = 0; i < query.length(); i++) {
char currentChar = query.charAt(i);
if (QUOTING_CHARACTERS.contains(currentChar)) {
if (inQuotation == null) {
inQuotation = currentChar;
start = i;
} else if (currentChar == inQuotation) {
inQuotation = null;
quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i)));
}
}
}
if (inQuotation != null) {
throw new IllegalArgumentException(
String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start));
}
}
/**
* Checks if a given index is within a quoted range.
*
* @param index to check if it is part of a quoted range.
* @return whether the query contains a quoted range at {@literal index}.
*/
public boolean isQuoted(int index) {
return quotedRanges.stream().anyMatch(r -> r.contains(index));
}
}
/**
* Evaluates Value expressions as detected by {@link ParsedQuery} based on parameter information from a method and
* parameter values from a method call.
*
* @author Jens Schauder
* @author Gerrit Meier
* @author Oliver Gierke
* @see ValueExpressionQueryRewriter#parse(String)
*/
public class QueryExpressionEvaluator {
private final ValueEvaluationContextProvider evaluationContextProvider;
private final ParsedQuery detector;
public QueryExpressionEvaluator(ValueEvaluationContextProvider evaluationContextProvider,
ParsedQuery detector) {
this.evaluationContextProvider = evaluationContextProvider;
this.detector = detector;
}
/**
* Evaluate all value expressions in {@link ParsedQuery} based on values provided as an argument.
*
* @param values Parameter values. Must not be {@literal null}.
* @return a map from parameter name to evaluated value as of {@link ParsedQuery#getParameterMap()}.
*/
public Map<String, Object> evaluate(Object[] values) {
Assert.notNull(values, "Values must not be null.");
Map<String, ValueExpression> parameterMap = detector.getParameterMap();
Map<String, Object> results = new LinkedHashMap<>(parameterMap.size());
parameterMap.forEach((parameter, expression) -> results.put(parameter, evaluate(expression, values)));
return results;
}
/**
* Returns the query string produced by the intermediate Value Expression collection step.
*
* @return
*/
public String getQueryString() {
return detector.getQueryString();
}
@Nullable
private Object evaluate(ValueExpression expression, Object[] values) {
ValueEvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(values,
expression.getExpressionDependencies());
return expression.evaluate(evaluationContext);
}
}
}

5
src/main/java/org/springframework/data/spel/EvaluationContextProvider.java

@ -17,6 +17,7 @@ package org.springframework.data.spel; @@ -17,6 +17,7 @@ package org.springframework.data.spel;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
/**
* Provides a way to access a centrally defined potentially shared {@link StandardEvaluationContext}.
@ -44,7 +45,7 @@ public interface EvaluationContextProvider { @@ -44,7 +45,7 @@ public interface EvaluationContextProvider {
* @param rootObject the root object to set in the {@link EvaluationContext}.
* @return
*/
EvaluationContext getEvaluationContext(Object rootObject);
EvaluationContext getEvaluationContext(@Nullable Object rootObject);
/**
* Return a tailored {@link EvaluationContext} built using the given parameter values and considering
@ -57,7 +58,7 @@ public interface EvaluationContextProvider { @@ -57,7 +58,7 @@ public interface EvaluationContextProvider {
* @return
* @since 2.4
*/
default EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
default EvaluationContext getEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) {
return getEvaluationContext(rootObject);
}
}

8
src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java

@ -101,16 +101,17 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex @@ -101,16 +101,17 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
}
@Override
public StandardEvaluationContext getEvaluationContext(Object rootObject) {
public StandardEvaluationContext getEvaluationContext(@Nullable Object rootObject) {
return doGetEvaluationContext(rootObject, getExtensions(Predicates.isTrue()));
}
@Override
public StandardEvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
public StandardEvaluationContext getEvaluationContext(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return doGetEvaluationContext(rootObject, getExtensions(it -> dependencies.stream().anyMatch(it::provides)));
}
StandardEvaluationContext doGetEvaluationContext(Object rootObject,
StandardEvaluationContext doGetEvaluationContext(@Nullable Object rootObject,
Collection<? extends EvaluationContextExtension> extensions) {
StandardEvaluationContext context = new StandardEvaluationContext();
@ -180,7 +181,6 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex @@ -180,7 +181,6 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
* Creates {@link EvaluationContextExtensionAdapter}s for the given {@link EvaluationContextExtension}s.
*
* @param extensions
* @param filter to remove unwanted extensions.
* @return
*/
private List<EvaluationContextExtensionAdapter> toAdapters(

5
src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java

@ -18,6 +18,7 @@ package org.springframework.data.spel; @@ -18,6 +18,7 @@ package org.springframework.data.spel;
import reactor.core.publisher.Mono;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
/**
* Provides a way to access a centrally defined potentially shared {@link EvaluationContext}.
@ -33,7 +34,7 @@ public interface ReactiveEvaluationContextProvider extends EvaluationContextProv @@ -33,7 +34,7 @@ public interface ReactiveEvaluationContextProvider extends EvaluationContextProv
* @param rootObject the root object to set in the {@link EvaluationContext}.
* @return a mono that emits exactly one {@link EvaluationContext}.
*/
Mono<? extends EvaluationContext> getEvaluationContextLater(Object rootObject);
Mono<? extends EvaluationContext> getEvaluationContextLater(@Nullable Object rootObject);
/**
* Return a tailored {@link EvaluationContext} built using the given parameter values and considering
@ -46,7 +47,7 @@ public interface ReactiveEvaluationContextProvider extends EvaluationContextProv @@ -46,7 +47,7 @@ public interface ReactiveEvaluationContextProvider extends EvaluationContextProv
* @return a mono that emits exactly one {@link EvaluationContext}.
* @since 2.4
*/
default Mono<? extends EvaluationContext> getEvaluationContextLater(Object rootObject,
default Mono<? extends EvaluationContext> getEvaluationContextLater(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return getEvaluationContextLater(rootObject);
}

5
src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java

@ -31,6 +31,7 @@ import org.springframework.data.util.Predicates; @@ -31,6 +31,7 @@ import org.springframework.data.util.Predicates;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
/**
* A reactive {@link EvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of
@ -81,13 +82,13 @@ public class ReactiveExtensionAwareEvaluationContextProvider implements Reactive @@ -81,13 +82,13 @@ public class ReactiveExtensionAwareEvaluationContextProvider implements Reactive
}
@Override
public Mono<StandardEvaluationContext> getEvaluationContextLater(Object rootObject) {
public Mono<StandardEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject) {
return getExtensions(Predicates.isTrue()) //
.map(it -> evaluationContextProvider.doGetEvaluationContext(rootObject, it));
}
@Override
public Mono<StandardEvaluationContext> getEvaluationContextLater(Object rootObject,
public Mono<StandardEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return getExtensions(it -> dependencies.stream().anyMatch(it::provides)) //

36
src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java

@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*; @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -28,7 +29,6 @@ import org.springframework.core.env.StandardEnvironment; @@ -28,7 +29,6 @@ import org.springframework.core.env.StandardEnvironment;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
@ -39,6 +39,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -39,6 +39,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
*/
public class ValueEvaluationUnitTests {
private ValueExpressionParser parser = ValueExpressionParser.create();
private ValueEvaluationContext evaluationContext;
@BeforeEach
@ -48,6 +49,10 @@ public class ValueEvaluationUnitTests { @@ -48,6 +49,10 @@ public class ValueEvaluationUnitTests {
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().addFirst(propertySource);
record MyRecord(String foo, @org.springframework.lang.Nullable String bar) {
}
this.evaluationContext = new ValueEvaluationContext() {
@Override
public Environment getEnvironment() {
@ -58,6 +63,8 @@ public class ValueEvaluationUnitTests { @@ -58,6 +63,8 @@ public class ValueEvaluationUnitTests {
public EvaluationContext getEvaluationContext() {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("contextVar", "contextVal");
context.setVariable("nullVar", null);
context.setVariable("someRecord", new MyRecord("foo", null));
return context;
}
@ -83,11 +90,24 @@ public class ValueEvaluationUnitTests { @@ -83,11 +90,24 @@ public class ValueEvaluationUnitTests {
.withMessageContaining("Could not resolve placeholder 'env.does.not.exist'");
}
@Test // GH-3169
void shouldReturnValueType() {
assertThat(getValueType("foo")).isEqualTo(String.class);
assertThat(getValueType("${env.key.one}")).isEqualTo(String.class);
assertThat(getValueType("#{'foo'}")).isEqualTo(String.class);
assertThat(getValueType("#{1+1}")).isEqualTo(Integer.class);
assertThat(getValueType("#{null}")).isNull();
assertThat(getValueType("#{#contextVar}")).isEqualTo(String.class);
assertThat(getValueType("#{#nullVar}")).isNull();
assertThat(getValueType("#{#someRecord.foo}")).isEqualTo(String.class);
assertThat(getValueType("#{#someRecord.bar}")).isEqualTo(String.class);
}
@Test // GH-2369
void shouldParseLiteral() {
ValueParserConfiguration parserContext = () -> new SpelExpressionParser();
ValueExpressionParser parser = ValueExpressionParser.create(parserContext);
ValueExpressionParser parser = ValueExpressionParser.create();
assertThat(parser.parse("#{'foo'}-${key.one}").isLiteral()).isFalse();
assertThat(parser.parse("foo").isLiteral()).isTrue();
@ -130,12 +150,14 @@ public class ValueEvaluationUnitTests { @@ -130,12 +150,14 @@ public class ValueEvaluationUnitTests {
assertThat(eval("#{(1+1) + \"-foo'}\" + '-bar}'}")).isEqualTo("2-foo'}-bar}");
}
@SuppressWarnings("DataFlowIssue")
private String eval(String expressionString) {
ValueParserConfiguration parserContext = SpelExpressionParser::new;
ValueExpressionParser parser = ValueExpressionParser.create(parserContext);
return (String) parser.parse(expressionString).evaluate(evaluationContext);
}
@Nullable
private Class<?> getValueType(String expressionString) {
return parser.parse(expressionString).getValueType(evaluationContext);
}
}

160
src/test/java/org/springframework/data/repository/query/ValueExpressionQueryRewriterUnitTests.java

@ -0,0 +1,160 @@ @@ -0,0 +1,160 @@
/*
* 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.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.function.BiFunction;
import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.spel.EvaluationContextProvider;
/**
* Unit tests for {@link ValueExpressionQueryRewriter}.
*
* @author Mark Paluch
*/
class ValueExpressionQueryRewriterUnitTests {
static final BiFunction<Integer, String, String> PARAMETER_NAME_SOURCE = (index, spel) -> "EPP" + index;
static final BiFunction<String, String, String> REPLACEMENT_SOURCE = (prefix, name) -> prefix + name;
static final ValueExpressionParser PARSER = ValueExpressionParser.create();
@Test // GH-3049
void nullQueryThrowsException() {
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE);
assertThatIllegalArgumentException().isThrownBy(() -> context.parse(null));
}
@Test // GH-3049
void emptyStringGetsParsedCorrectly() {
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE);
var extractor = context.parse("");
assertThat(extractor.getQueryString()).isEqualTo("");
assertThat(extractor.getParameterMap()).isEmpty();
}
@Test // GH-3049
void findsAndReplacesExpressions() {
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE);
var extractor = context.parse(":#{one} ?#{two} :${three} ?${four}");
assertThat(extractor.getQueryString()).isEqualTo(":EPP0 ?EPP1 :EPP2 ?EPP3");
assertThat(extractor.getParameterMap().entrySet()) //
.extracting(Map.Entry::getKey, it -> it.getValue().getExpressionString()) //
.containsExactlyInAnyOrder( //
Tuple.tuple("EPP0", "one"), //
Tuple.tuple("EPP1", "two"), //
Tuple.tuple("EPP2", "${three}"), //
Tuple.tuple("EPP3", "${four}") //
);
}
@Test // GH-3049
void keepsStringWhenNoMatchIsFound() {
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE);
var extractor = context.parse("abcdef");
assertThat(extractor.getQueryString()).isEqualTo("abcdef");
assertThat(extractor.getParameterMap()).isEmpty();
}
@Test // GH-3049
void spelsInQuotesGetIgnored() {
var queries = Arrays.asList(//
"a'b:#{one}cd'ef", //
"a'b:#{o'ne}cdef", //
"ab':#{one}'cdef", //
"ab:'#{one}cd'ef", //
"ab:#'{one}cd'ef", //
"a'b:#{o'ne}cdef");
queries.forEach(this::checkNoExpressionIsFound);
}
private void checkNoExpressionIsFound(String query) {
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE);
var extractor = context.parse(query);
assertThat(extractor.getQueryString()).describedAs(query).isEqualTo(query);
assertThat(extractor.getParameterMap()).describedAs(query).isEmpty();
}
@Test // GH-3049
void shouldEvaluateExpression() throws Exception {
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().addFirst(new MapPropertySource("synthetic", Map.of("foo", "world")));
QueryMethodValueEvaluationContextAccessor contextAccessor = new QueryMethodValueEvaluationContextAccessor(
environment,
EvaluationContextProvider.DEFAULT);
ValueExpressionDelegate delegate = new ValueExpressionDelegate(contextAccessor, PARSER);
ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter
.of(delegate, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE);
Method method = ValueExpressionQueryRewriterUnitTests.MyRepository.class.getDeclaredMethod("simpleExpression",
String.class);
var extractor = rewriter.parse("SELECT :#{#value}, :${foo}",
new DefaultParameters(ParametersSource.of(method)));
assertThat(extractor.getQueryString()).isEqualTo("SELECT :EPP0, :EPP1");
assertThat(extractor.evaluate(new Object[] { "hello" })).containsEntry("EPP0", "hello").containsEntry("EPP1",
"world");
}
@Test // GH-3049
void shouldAllowNullValues() throws Exception {
ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE,
REPLACEMENT_SOURCE);
StandardEnvironment environment = new StandardEnvironment();
QueryMethodValueEvaluationContextAccessor factory = new QueryMethodValueEvaluationContextAccessor(environment,
EvaluationContextProvider.DEFAULT);
Method method = ValueExpressionQueryRewriterUnitTests.MyRepository.class.getDeclaredMethod("simpleExpression",
String.class);
var extractor = rewriter.withEvaluationContextAccessor(factory).parse("SELECT :#{#value}",
new DefaultParameters(ParametersSource.of(method)));
assertThat(extractor.getQueryString()).isEqualTo("SELECT :EPP0");
assertThat(extractor.evaluate(new Object[] { null })).containsEntry("EPP0", null);
}
interface MyRepository {
void simpleExpression(String value);
}
}
Loading…
Cancel
Save