Skip to content

Commit

Permalink
Add ValueExpression infrastructure for query methods.
Browse files Browse the repository at this point in the history
Introduce ValueExpressionQueryRewriter as replacement for SpelQueryContext and QueryMethodValueEvaluationContextAccessor to encapsulate common ValueExpression functionality for Spring Data modules wanting to resolve Value Expressions in query methods.

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

Deprecate QueryMethodEvaluationContextProvider and its reactive variant for future removal.

Closes #3049
Original pull request: #3050
  • Loading branch information
mp911de committed Oct 9, 2024
1 parent e9ae6c7 commit 096836e
Show file tree
Hide file tree
Showing 31 changed files with 1,506 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,10 @@ public String evaluate(ValueEvaluationContext context) {

return builder.toString();
}

@Override
public Class<?> getValueType(ValueEvaluationContext context) {
return String.class;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@

import org.springframework.core.env.Environment;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;

/**
* Default {@link ValueEvaluationContext}.
*
* @author Mark Paluch
* @since 3.3
*/
record DefaultValueEvaluationContext(Environment environment,
record DefaultValueEvaluationContext(@Nullable Environment environment,
EvaluationContext evaluationContext) implements ValueEvaluationContext {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ public String evaluate(ValueEvaluationContext context) {
return expression;
}

@Override
public Class<?> getValueType(ValueEvaluationContext context) {
return String.class;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -51,4 +51,9 @@ public Object evaluate(ValueEvaluationContext context) {
return expression;
}

@Override
public Class<?> getValueType(ValueEvaluationContext context) {
return String.class;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.expression;

import reactor.core.publisher.Mono;

import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.lang.Nullable;

/**
* Reactive extension to {@link ValueEvaluationContext} for obtaining a {@link ValueEvaluationContext} that participates
* in the reactive flow.
*
* @author Mark Paluch
* @since 3.4
*/
public interface ReactiveValueEvaluationContextProvider extends ValueEvaluationContextProvider {

/**
* Return a {@link ValueEvaluationContext} built using the given parameter values.
*
* @param rootObject the root object to set in the {@link ValueEvaluationContext}.
* @return a mono that emits exactly one {@link ValueEvaluationContext}.
*/
Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject);

/**
* Return a tailored {@link ValueEvaluationContext} built using the given parameter values and considering
* {@link ExpressionDependencies expression dependencies}. The returned {@link ValueEvaluationContext} may contain a
* reduced visibility of methods and properties/fields according to the required {@link ExpressionDependencies
* expression dependencies}.
*
* @param rootObject the root object to set in the {@link ValueEvaluationContext}.
* @param dependencies the requested expression dependencies to be available.
* @return a mono that emits exactly one {@link ValueEvaluationContext}.
*/
default Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject,
ExpressionDependencies dependencies) {
return getEvaluationContextLater(rootObject);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@

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}.
*
* @author Mark Paluch
* @since 3.3
*/
@FunctionalInterface
public interface ValueEvaluationContextProvider {

/**
Expand All @@ -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
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {

Expand All @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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.
* <p>
* This method delegates to
* {@link #getQueryLookupStrategy(QueryLookupStrategy.Key, QueryMethodEvaluationContextProvider)} unless overridden.
* </p>
*
* @param key can be {@literal null}.
* @param valueExpressionDelegate will never be {@literal null}.
* @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up.
* @since 3.4
*/
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable QueryLookupStrategy.Key key,
ValueExpressionDelegate valueExpressionDelegate) {
return getQueryLookupStrategy(key,
new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(getEvaluationContextProvider()));
}

/**
* We need to make sure that the necessary conversion libraries are in place if the repository interface uses RxJava 1
* types.
Expand Down
Loading

0 comments on commit 096836e

Please sign in to comment.