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 extends T> 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 extends Evaluat
Assert.notNull(extensions, "EvaluationContextExtensions must not be null");
- this.delegate = new org.springframework.data.spel.ExtensionAwareEvaluationContextProvider(extensions);
+ this.delegate = new QueryMethodValueEvaluationContextAccessor(QueryMethodValueEvaluationContextAccessor.ENVIRONMENT,
+ extensions);
}
- @Override
- public > 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 extends ExtensionIdAware> extensions) {
+
+ Assert.notNull(extensions, "EvaluationContextExtensions must not be null");
+
+ this.environment = environment;
+ this.evaluationContextProvider = createEvaluationContextProvider(extensions);
+ }
+
+ private static EvaluationContextProvider createEvaluationContextProvider(ListableBeanFactory factory) {
+
+ return ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)
+ ? new ReactiveExtensionAwareEvaluationContextProvider(factory)
+ : new ExtensionAwareEvaluationContextProvider(factory);
+
+ }
+
+ private static EvaluationContextProvider createEvaluationContextProvider(
+ Collection extends ExtensionIdAware> extensions) {
+
+ return ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)
+ ? new ReactiveExtensionAwareEvaluationContextProvider(extensions)
+ : new ExtensionAwareEvaluationContextProvider(extensions);
+ }
+
+ /**
+ * Creates a default {@link QueryMethodValueEvaluationContextAccessor} using the
+ * {@link org.springframework.core.env.StandardEnvironment} and extension-less
+ * {@link org.springframework.data.spel.EvaluationContextProvider}.
+ *
+ * @return a default {@link ValueExpressionDelegate}.
+ */
+ public static QueryMethodValueEvaluationContextAccessor create() {
+ return new QueryMethodValueEvaluationContextAccessor(ENVIRONMENT, DEFAULT_CONTEXT_PROVIDER);
+ }
+
+ EvaluationContextProvider getEvaluationContextProvider() {
+ return evaluationContextProvider;
+ }
+
+ /**
+ * Creates a new {@link ValueEvaluationContextProvider} for the given {@link Parameters}.
+ *
+ * @param parameters must not be {@literal null}.
+ * @return a new {@link ValueEvaluationContextProvider} for the given {@link Parameters}.
+ */
+ public ValueEvaluationContextProvider create(Parameters, ?> parameters) {
+
+ Assert.notNull(parameters, "Parameters must not be null");
+
+ if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) {
+ if (evaluationContextProvider instanceof ReactiveEvaluationContextProvider reactive) {
+ return new DefaultReactiveQueryMethodValueEvaluationContextProvider(environment, parameters, reactive);
+ }
+ }
+
+ return new DefaultQueryMethodValueEvaluationContextProvider(environment, parameters, evaluationContextProvider);
+ }
+
+ /**
+ * Exposes variables for all named parameters for the given arguments. Also exposes non-bindable parameters under the
+ * names of their types.
+ *
+ * @param parameters must not be {@literal null}.
+ * @param arguments must not be {@literal null}.
+ * @return
+ */
+ static Map 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 extends ExtensionIdAware> 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 extends EvaluationContextExtension> extensions) {
StandardEvaluationContext context = new StandardEvaluationContext();
@@ -180,7 +181,6 @@ EvaluationContextExtensionInformation getOrCreateInformation(Class extends Eva
* Creates {@link EvaluationContextExtensionAdapter}s for the given {@link EvaluationContextExtension}s.
*
* @param extensions
- * @param filter to remove unwanted extensions.
* @return
*/
private List 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 extends EvaluationContext> getEvaluationContextLater(Object rootObject);
+ Mono extends EvaluationContext> getEvaluationContextLater(@Nullable Object rootObject);
/**
* Return a tailored {@link EvaluationContext} built using the given parameter values and considering
@@ -46,7 +47,7 @@ public interface ReactiveEvaluationContextProvider extends EvaluationContextProv
* @return a mono that emits exactly one {@link EvaluationContext}.
* @since 2.4
*/
- default Mono extends EvaluationContext> getEvaluationContextLater(Object rootObject,
+ default Mono extends EvaluationContext> getEvaluationContextLater(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return getEvaluationContextLater(rootObject);
}
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);
+
+ }
+}