diff --git a/pom.xml b/pom.xml index c6dc5a59a0..d769596164 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.4.0-SNAPSHOT + 3.4.0-GH-3049-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. diff --git a/src/main/java/org/springframework/data/expression/CompositeValueExpression.java b/src/main/java/org/springframework/data/expression/CompositeValueExpression.java index d2dc64a48a..e2224d5f99 100644 --- a/src/main/java/org/springframework/data/expression/CompositeValueExpression.java +++ b/src/main/java/org/springframework/data/expression/CompositeValueExpression.java @@ -72,4 +72,10 @@ public String evaluate(ValueEvaluationContext context) { return builder.toString(); } + + @Override + public Class getValueType(ValueEvaluationContext context) { + return String.class; + } + } diff --git a/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java b/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java index d498560135..c0f72a1000 100644 --- a/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java +++ b/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java @@ -17,6 +17,7 @@ import org.springframework.core.env.Environment; import org.springframework.expression.EvaluationContext; +import org.springframework.lang.Nullable; /** * Default {@link ValueEvaluationContext}. @@ -24,7 +25,7 @@ * @author Mark Paluch * @since 3.3 */ -record DefaultValueEvaluationContext(Environment environment, +record DefaultValueEvaluationContext(@Nullable Environment environment, EvaluationContext evaluationContext) implements ValueEvaluationContext { @Override diff --git a/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java b/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java index ceea642641..5fd7ad192d 100644 --- a/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java +++ b/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java @@ -22,6 +22,7 @@ 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 { 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) { diff --git a/src/main/java/org/springframework/data/expression/ExpressionExpression.java b/src/main/java/org/springframework/data/expression/ExpressionExpression.java index 83da26614e..1702f4eb1e 100644 --- a/src/main/java/org/springframework/data/expression/ExpressionExpression.java +++ b/src/main/java/org/springframework/data/expression/ExpressionExpression.java @@ -47,9 +47,14 @@ public boolean isLiteral() { 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(); + } + } diff --git a/src/main/java/org/springframework/data/expression/LiteralValueExpression.java b/src/main/java/org/springframework/data/expression/LiteralValueExpression.java index 764220bfc6..2b50f91f77 100644 --- a/src/main/java/org/springframework/data/expression/LiteralValueExpression.java +++ b/src/main/java/org/springframework/data/expression/LiteralValueExpression.java @@ -39,4 +39,9 @@ public String evaluate(ValueEvaluationContext context) { return expression; } + @Override + public Class getValueType(ValueEvaluationContext context) { + return String.class; + } + } diff --git a/src/main/java/org/springframework/data/expression/PlaceholderExpression.java b/src/main/java/org/springframework/data/expression/PlaceholderExpression.java index 1473f1ed28..f58abfc0e4 100644 --- a/src/main/java/org/springframework/data/expression/PlaceholderExpression.java +++ b/src/main/java/org/springframework/data/expression/PlaceholderExpression.java @@ -38,7 +38,7 @@ public boolean isLiteral() { } @Override - public Object evaluate(ValueEvaluationContext context) { + public String evaluate(ValueEvaluationContext context) { Environment environment = context.getEnvironment(); if (environment != null) { @@ -51,4 +51,9 @@ public Object evaluate(ValueEvaluationContext context) { return expression; } + @Override + public Class getValueType(ValueEvaluationContext context) { + return String.class; + } + } diff --git a/src/main/java/org/springframework/data/expression/ReactiveValueEvaluationContextProvider.java b/src/main/java/org/springframework/data/expression/ReactiveValueEvaluationContextProvider.java new file mode 100644 index 0000000000..a280ddf625 --- /dev/null +++ b/src/main/java/org/springframework/data/expression/ReactiveValueEvaluationContextProvider.java @@ -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 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 getEvaluationContextLater(@Nullable Object rootObject, + ExpressionDependencies dependencies) { + return getEvaluationContextLater(rootObject); + } +} diff --git a/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java b/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java index 5be091d8ca..42bc6a289e 100644 --- a/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java +++ b/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java @@ -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 @@ static ValueEvaluationContext of(Environment environment, EvaluationContext eval /** * 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; + } } diff --git a/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java b/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java index 05d7d4de2f..ec7bbd10a0 100644 --- a/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java @@ -17,6 +17,7 @@ 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 @@ * @author Mark Paluch * @since 3.3 */ +@FunctionalInterface 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 { * @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); } } diff --git a/src/main/java/org/springframework/data/expression/ValueExpression.java b/src/main/java/org/springframework/data/expression/ValueExpression.java index 8f4545bcd8..7ad6ad4d26 100644 --- a/src/main/java/org/springframework/data/expression/ValueExpression.java +++ b/src/main/java/org/springframework/data/expression/ValueExpression.java @@ -62,4 +62,15 @@ default ExpressionDependencies getExpressionDependencies() { @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; + } diff --git a/src/main/java/org/springframework/data/expression/ValueExpressionParser.java b/src/main/java/org/springframework/data/expression/ValueExpressionParser.java index 89f9f65cdc..b5e1645899 100644 --- a/src/main/java/org/springframework/data/expression/ValueExpressionParser.java +++ b/src/main/java/org/springframework/data/expression/ValueExpressionParser.java @@ -27,6 +27,16 @@ */ 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. * diff --git a/src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java b/src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java index 91be052dc2..8f4b075f20 100644 --- a/src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java +++ b/src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java @@ -23,6 +23,7 @@ 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 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 CachingValueExpressionEvaluatorFactory(ExpressionParser expressionParser, } @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)); } diff --git a/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java index b0a6ed332d..45e3ba34ea 100644 --- a/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java @@ -17,14 +17,20 @@ 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 void setEvaluationContextProvider(QueryMethodEvaluationContextProvider ev : 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. + *

+ * This method delegates to + * {@link #getQueryLookupStrategy(QueryLookupStrategy.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 + */ + @Override + protected Optional 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. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java index b1a8f155ff..967d02d158 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java @@ -28,6 +28,8 @@ 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.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 @@ * @author Johannes Englmeier */ public abstract class RepositoryFactoryBeanSupport, S, ID> - implements InitializingBean, RepositoryFactoryInformation, FactoryBean, BeanClassLoaderAware, - BeanFactoryAware, ApplicationEventPublisherAware { + implements InitializingBean, RepositoryFactoryInformation, FactoryBean, ApplicationEventPublisherAware, + BeanClassLoaderAware, BeanFactoryAware, EnvironmentAware { private final Class repositoryInterface; @@ -74,14 +78,15 @@ public abstract class RepositoryFactoryBeanSupport, private Optional> repositoryBaseClass = Optional.empty(); private Optional customImplementation = Optional.empty(); private Optional repositoryFragments = Optional.empty(); - private NamedQueries namedQueries; + private NamedQueries namedQueries = PropertiesBasedNamedQueries.EMPTY; private Optional> mappingContext = Optional.empty(); private ClassLoader classLoader; + private ApplicationEventPublisher publisher; private BeanFactory beanFactory; + private Environment environment; private boolean lazyInit = false; - private Optional evaluationContextProvider = Optional.empty(); - private List repositoryFactoryCustomizers = new ArrayList<>(); - private ApplicationEventPublisher publisher; + private Optional evaluationContextProvider = Optional.empty(); + private final List repositoryFactoryCustomizers = new ArrayList<>(); private Lazy repository; @@ -115,7 +120,7 @@ public void setRepositoryBaseClass(Class repositoryBaseClass) { *

* Default is "false", in order to avoid unnecessary extra interception. This means that no guarantees are provided * that {@code RepositoryMethodContext} access will work consistently within any method of the advised object. - * + * * @since 3.4.0 */ public void setExposeMetadata(boolean exposeMetadata) { @@ -168,14 +173,26 @@ protected void setMappingContext(MappingContext mappingContext) { 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 void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; - if (!this.evaluationContextProvider.isPresent() && ListableBeanFactory.class.isInstance(beanFactory)) { - 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 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 createDefaultQueryMethodEvaluationContextProvider( ListableBeanFactory beanFactory) { return Optional.of(new ExtensionAwareQueryMethodEvaluationContextProvider(beanFactory)); @@ -233,11 +269,13 @@ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } + @Override @SuppressWarnings("unchecked") public EntityInformation getEntityInformation() { return (EntityInformation) factory.getEntityInformation(repositoryMetadata.getDomainType()); } + @Override public RepositoryInformation getRepositoryInformation() { RepositoryFragments fragments = customImplementation.map(RepositoryFragments::just)// @@ -246,30 +284,36 @@ public RepositoryInformation getRepositoryInformation() { return factory.getRepositoryInformation(repositoryMetadata, fragments); } + @Override public PersistentEntity getPersistentEntity() { return mappingContext.orElseThrow(() -> new IllegalStateException("No MappingContext available")) .getRequiredPersistentEntity(repositoryMetadata.getDomainType()); } + @Override public List getQueryMethods() { return factory.getQueryMethods(); } + @Override @NonNull public T getObject() { return this.repository.get(); } + @Override @NonNull public Class getObjectType() { return repositoryInterface; } + @Override public boolean isSingleton() { return true; } + @Override public void afterPropertiesSet() { this.factory = createRepositoryFactory(); @@ -277,14 +321,18 @@ public void afterPropertiesSet() { 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)); diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 0b73d66a0f..1c34a90b75 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -38,11 +38,16 @@ 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.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 @@ * @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, private final Map repositoryInformationCache; private final List postProcessors; - private Optional> repositoryBaseClass; + private @Nullable Class repositoryBaseClass; private boolean exposeMetadata; private @Nullable QueryLookupStrategy.Key queryLookupStrategyKey; - private List> queryPostProcessors; - private List methodInvocationListeners; + private final List> queryPostProcessors; + private final List methodInvocationListeners; private NamedQueries namedQueries; private ClassLoader classLoader; - private QueryMethodEvaluationContextProvider evaluationContextProvider; + private EvaluationContextProvider evaluationContextProvider; private BeanFactory beanFactory; + private Environment environment; private Lazy projectionFactory; private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); @@ -112,16 +128,19 @@ public RepositoryFactorySupport() { 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 void setExposeMetadata(boolean exposeMetadata) { } /** - * 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 void setQueryLookupStrategyKey(Key key) { * * @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 void setBeanFactory(BeanFactory beanFactory) throws BeansException { 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 void setEvaluationContextProvider(QueryMethodEvaluationContextProvider ev * @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 void addQueryCreationListener(QueryCreationListener listener) { * 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 void addInvocationListener(RepositoryMethodInvocationListener listener) { * the proxy gets created. Note that the {@link QueryExecutorMethodInterceptor} will be added to the proxy * after 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 void addRepositoryProxyPostProcessor(RepositoryProxyPostProcessor process /** * 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 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata /** * 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 @@ private RepositoryComposition getRepositoryComposition(RepositoryMetadata metada * Returns a repository instance for the given interface. * * @param repositoryInterface must not be {@literal null}. - * @return + * @return the implemented repository interface. */ public T getRepository(Class repositoryInterface) { return getRepository(repositoryInterface, RepositoryFragments.empty()); @@ -269,7 +315,7 @@ public T getRepository(Class repositoryInterface) { * * @param repositoryInterface must not be {@literal null}. * @param customImplementation must not be {@literal null}. - * @return + * @return the implemented repository interface. */ public T getRepository(Class repositoryInterface, Object customImplementation) { return getRepository(repositoryInterface, RepositoryFragments.just(customImplementation)); @@ -281,7 +327,7 @@ public T getRepository(Class repositoryInterface, Object customImplementa * * @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 T getRepository(Class repositoryInterface, RepositoryFragments fra 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 T getRepository(Class repositoryInterface, RepositoryFragments fra } Optional 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 T getRepository(Class repositoryInterface, RepositoryFragments fra */ 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 @@ private RepositoryInformation getRepositoryInformation(RepositoryMetadata metada 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 @@ protected ProjectionFactory getProjectionFactory() { * @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 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. + *

+ * 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 getQueryLookupStrategy(@Nullable Key key, + ValueExpressionDelegate valueExpressionDelegate) { + return getQueryLookupStrategy(key, + new ExtensionAwareQueryMethodEvaluationContextProvider(evaluationContextProvider)); + } + /** * Validates the given repository interface as well as the given custom implementation. * diff --git a/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryFactoryBeanSupport.java index 2bd5163353..3b45a45157 100644 --- a/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryFactoryBeanSupport.java @@ -101,6 +101,7 @@ protected final RepositoryFactorySupport createRepositoryFactory() { */ protected abstract RepositoryFactorySupport doCreateRepositoryFactory(); + @Override public void setBeanFactory(BeanFactory beanFactory) { Assert.isInstanceOf(ListableBeanFactory.class, beanFactory); diff --git a/src/main/java/org/springframework/data/repository/query/CachingValueExpressionDelegate.java b/src/main/java/org/springframework/data/repository/query/CachingValueExpressionDelegate.java new file mode 100644 index 0000000000..e961d133ed --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/CachingValueExpressionDelegate.java @@ -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 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); + } +} diff --git a/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java index 2897872fe0..bc30d81564 100644 --- a/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java @@ -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 @@ * @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 ExtensionAwareQueryMethodEvaluationContextProvider(ListableBeanFactory be 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 ExtensionAwareQueryMethodEvaluationContextProvider(List> 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 > EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues, - ExpressionDependencies dependencies) { - - StandardEvaluationContext evaluationContext = delegate.getEvaluationContext(parameterValues, dependencies); + public > 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 collectVariables(Parameters parameters, Object[] arguments) { - - Map 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 > 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(); } } diff --git a/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java index 3d4e165be8..76dcd38f2b 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java @@ -17,6 +17,7 @@ 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 @@ * @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 { */ > EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues, ExpressionDependencies dependencies); + + /** + * @return the underlying {@link EvaluationContextProvider}. + */ + EvaluationContextProvider getEvaluationContextProvider(); } diff --git a/src/main/java/org/springframework/data/repository/query/QueryMethodValueEvaluationContextAccessor.java b/src/main/java/org/springframework/data/repository/query/QueryMethodValueEvaluationContextAccessor.java new file mode 100644 index 0000000000..d4773afaf9 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/QueryMethodValueEvaluationContextAccessor.java @@ -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 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 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 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 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 getEvaluationContextLater(@Nullable Object rootObject) { + return delegate.getEvaluationContextLater(rootObject).map(it -> doGetEvaluationContext(it, rootObject)); + } + + @Override + public Mono getEvaluationContextLater(@Nullable Object rootObject, + ExpressionDependencies dependencies) { + return delegate.getEvaluationContextLater(rootObject, dependencies) + .map(it -> doGetEvaluationContext(it, rootObject)); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java index 1a1d2a8b07..283c091882 100644 --- a/src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java @@ -20,13 +20,13 @@ 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 @@ * * @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 * 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 ReactiveExtensionAwareQueryMethodEvaluationContextProvider(ListableBeanFa * @param extensions must not be {@literal null}. */ public ReactiveExtensionAwareQueryMethodEvaluationContextProvider(List extensions) { - - Assert.notNull(extensions, "EvaluationContextExtensions must not be null"); - - this.delegate = new ReactiveExtensionAwareEvaluationContextProvider(extensions); - } - - @Override - public > 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 > 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 > Mono getEvaluationContextLater(T parameters, Object[] parameterValues) { - Mono 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 > Mono getEvaluationContextLater(T parameters, Object[] parameterValues, ExpressionDependencies dependencies) { - Mono 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); } } diff --git a/src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java index 02bba3d022..9e10bc0d82 100644 --- a/src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java @@ -28,7 +28,9 @@ * * @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( diff --git a/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java b/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java index 8142187910..7d1a5f5eef 100644 --- a/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java +++ b/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java @@ -35,7 +35,9 @@ * @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(); diff --git a/src/main/java/org/springframework/data/repository/query/SpelQueryContext.java b/src/main/java/org/springframework/data/repository/query/SpelQueryContext.java index ac35839ebb..43a67809fc 100644 --- a/src/main/java/org/springframework/data/repository/query/SpelQueryContext.java +++ b/src/main/java/org/springframework/data/repository/query/SpelQueryContext.java @@ -58,7 +58,9 @@ * @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 = "([:?])#\\{([^}]+)}"; diff --git a/src/main/java/org/springframework/data/repository/query/ValueExpressionDelegate.java b/src/main/java/org/springframework/data/repository/query/ValueExpressionDelegate.java new file mode 100644 index 0000000000..17dbdcaf8e --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ValueExpressionDelegate.java @@ -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. + *

+ * 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); + } +} diff --git a/src/main/java/org/springframework/data/repository/query/ValueExpressionQueryRewriter.java b/src/main/java/org/springframework/data/repository/query/ValueExpressionQueryRewriter.java new file mode 100644 index 0000000000..092545729d --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ValueExpressionQueryRewriter.java @@ -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. + *

+ * 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}. + *

+ * Typical usage looks like + * + *

+ * 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);
+ * 
+ * + * @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 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 replacementSource; + + private ValueExpressionQueryRewriter(ValueExpressionParser expressionParser, + BiFunction parameterNameSource, BiFunction 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 parameterNameSource, BiFunction 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 parameterNameSource, BiFunction 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: + * + *
+	 * <prefix>#{<spel>}
+	 * <prefix>${<property placeholder>}
+	 * 
+ *

+ * 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 parameterNameSource, BiFunction replacementSource) { + + super(expressionParser, parameterNameSource, replacementSource); + + this.contextProviderFactory = factory; + } + + /** + * Parses the query for Value Expressions using the pattern: + * + *

+		 * <prefix>#{<spel>}
+		 * <prefix>${<property placeholder>}
+		 * 
+ *

+ * 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. + *

+ * 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 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 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 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 QUOTING_CHARACTERS = Arrays.asList('"', '\''); + + private final List> 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 evaluate(Object[] values) { + + Assert.notNull(values, "Values must not be null."); + + Map parameterMap = detector.getParameterMap(); + Map 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); + } + } +} diff --git a/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java index fe8e17a4ce..ff9905b4e4 100644 --- a/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java @@ -17,6 +17,7 @@ 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 { * @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 { * @return * @since 2.4 */ - default EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + default EvaluationContext getEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) { return getEvaluationContext(rootObject); } } diff --git a/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java index 67afc5e0ff..4ffb06adcd 100644 --- a/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java @@ -101,16 +101,17 @@ public ExtensionAwareEvaluationContextProvider( } @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 extensions) { StandardEvaluationContext context = new StandardEvaluationContext(); @@ -180,7 +181,6 @@ EvaluationContextExtensionInformation getOrCreateInformation(Class toAdapters( diff --git a/src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java index 062781a826..a1fd3a1c24 100644 --- a/src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java @@ -18,6 +18,7 @@ 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 * @param rootObject the root object to set in the {@link EvaluationContext}. * @return a mono that emits exactly one {@link EvaluationContext}. */ - Mono getEvaluationContextLater(Object rootObject); + Mono 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 * @return a mono that emits exactly one {@link EvaluationContext}. * @since 2.4 */ - default Mono getEvaluationContextLater(Object rootObject, + default Mono getEvaluationContextLater(@Nullable Object rootObject, ExpressionDependencies dependencies) { return getEvaluationContextLater(rootObject); } diff --git a/src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java index 935678f766..bdc68bfb59 100644 --- a/src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java @@ -31,6 +31,7 @@ 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 EvaluationContext getEvaluationContext(Object rootObject, ExpressionDepen } @Override - public Mono getEvaluationContextLater(Object rootObject) { + public Mono getEvaluationContextLater(@Nullable Object rootObject) { return getExtensions(Predicates.isTrue()) // .map(it -> evaluationContextProvider.doGetEvaluationContext(rootObject, it)); } @Override - public Mono getEvaluationContextLater(Object rootObject, + public Mono getEvaluationContextLater(@Nullable Object rootObject, ExpressionDependencies dependencies) { return getExtensions(it -> dependencies.stream().anyMatch(it::provides)) // diff --git a/src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java b/src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java index 59269b2b4a..22b65073b7 100644 --- a/src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java +++ b/src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java @@ -19,6 +19,7 @@ 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.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 @@ */ public class ValueEvaluationUnitTests { + private ValueExpressionParser parser = ValueExpressionParser.create(); private ValueEvaluationContext evaluationContext; @BeforeEach @@ -48,6 +49,10 @@ void setUp() { 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 Environment getEnvironment() { 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 @@ void shouldParseAndEvaluateExpressions() { .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 @@ void shouldParseQuoted() { 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); + } + } diff --git a/src/test/java/org/springframework/data/repository/query/ValueExpressionQueryRewriterUnitTests.java b/src/test/java/org/springframework/data/repository/query/ValueExpressionQueryRewriterUnitTests.java new file mode 100644 index 0000000000..68f0b35579 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/query/ValueExpressionQueryRewriterUnitTests.java @@ -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 PARAMETER_NAME_SOURCE = (index, spel) -> "EPP" + index; + static final BiFunction 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); + + } +}