Browse Source
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: #3050pull/3176/head
31 changed files with 1506 additions and 162 deletions
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
/* |
||||
* Copyright 2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.expression; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.data.spel.ExpressionDependencies; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* Reactive extension to {@link ValueEvaluationContext} for obtaining a {@link ValueEvaluationContext} that participates |
||||
* in the reactive flow. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 3.4 |
||||
*/ |
||||
public interface ReactiveValueEvaluationContextProvider extends ValueEvaluationContextProvider { |
||||
|
||||
/** |
||||
* Return a {@link ValueEvaluationContext} built using the given parameter values. |
||||
* |
||||
* @param rootObject the root object to set in the {@link ValueEvaluationContext}. |
||||
* @return a mono that emits exactly one {@link ValueEvaluationContext}. |
||||
*/ |
||||
Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject); |
||||
|
||||
/** |
||||
* Return a tailored {@link ValueEvaluationContext} built using the given parameter values and considering |
||||
* {@link ExpressionDependencies expression dependencies}. The returned {@link ValueEvaluationContext} may contain a |
||||
* reduced visibility of methods and properties/fields according to the required {@link ExpressionDependencies |
||||
* expression dependencies}. |
||||
* |
||||
* @param rootObject the root object to set in the {@link ValueEvaluationContext}. |
||||
* @param dependencies the requested expression dependencies to be available. |
||||
* @return a mono that emits exactly one {@link ValueEvaluationContext}. |
||||
*/ |
||||
default Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject, |
||||
ExpressionDependencies dependencies) { |
||||
return getEvaluationContextLater(rootObject); |
||||
} |
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
/* |
||||
* Copyright 2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.query; |
||||
|
||||
import org.springframework.data.expression.ValueExpression; |
||||
import org.springframework.data.expression.ValueExpressionParser; |
||||
import org.springframework.expression.ParseException; |
||||
import org.springframework.util.ConcurrentLruCache; |
||||
|
||||
/** |
||||
* Caching variant of {@link ValueExpressionDelegate}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 3.4 |
||||
*/ |
||||
public class CachingValueExpressionDelegate extends ValueExpressionDelegate { |
||||
|
||||
private final ConcurrentLruCache<String, ValueExpression> expressionCache; |
||||
|
||||
/** |
||||
* Creates a new {@link CachingValueExpressionDelegate} given {@link ValueExpressionDelegate}. |
||||
* |
||||
* @param delegate must not be {@literal null}. |
||||
*/ |
||||
public CachingValueExpressionDelegate(ValueExpressionDelegate delegate) { |
||||
super(delegate); |
||||
this.expressionCache = new ConcurrentLruCache<>(256, delegate.getValueExpressionParser()::parse); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link CachingValueExpressionDelegate} given {@link QueryMethodValueEvaluationContextAccessor} and |
||||
* {@link ValueExpressionParser}. |
||||
* |
||||
* @param providerFactory the factory to create value evaluation context providers, must not be {@code null}. |
||||
* @param valueExpressionParser the parser to parse expression strings into value expressions, must not be |
||||
* {@code null}. |
||||
*/ |
||||
public CachingValueExpressionDelegate(QueryMethodValueEvaluationContextAccessor providerFactory, |
||||
ValueExpressionParser valueExpressionParser) { |
||||
super(providerFactory, valueExpressionParser); |
||||
this.expressionCache = new ConcurrentLruCache<>(256, valueExpressionParser::parse); |
||||
} |
||||
|
||||
@Override |
||||
public ValueExpressionParser getValueExpressionParser() { |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ValueExpression parse(String expressionString) throws ParseException { |
||||
return expressionCache.get(expressionString); |
||||
} |
||||
} |
||||
@ -0,0 +1,269 @@
@@ -0,0 +1,269 @@
|
||||
/* |
||||
* Copyright 2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.query; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import org.springframework.beans.factory.ListableBeanFactory; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.core.env.StandardEnvironment; |
||||
import org.springframework.data.expression.ReactiveValueEvaluationContextProvider; |
||||
import org.springframework.data.expression.ValueEvaluationContext; |
||||
import org.springframework.data.expression.ValueEvaluationContextProvider; |
||||
import org.springframework.data.spel.EvaluationContextProvider; |
||||
import org.springframework.data.spel.ExpressionDependencies; |
||||
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; |
||||
import org.springframework.data.spel.ReactiveEvaluationContextProvider; |
||||
import org.springframework.data.spel.ReactiveExtensionAwareEvaluationContextProvider; |
||||
import org.springframework.data.spel.spi.EvaluationContextExtension; |
||||
import org.springframework.data.spel.spi.ExtensionIdAware; |
||||
import org.springframework.data.util.ReactiveWrappers; |
||||
import org.springframework.expression.EvaluationContext; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Factory to create {@link ValueEvaluationContextProvider} instances. Supports its reactive variant |
||||
* {@link ReactiveValueEvaluationContextProvider} if the underlying {@link EvaluationContextProvider} is a reactive one. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 3.4 |
||||
*/ |
||||
public class QueryMethodValueEvaluationContextAccessor { |
||||
|
||||
public static final EvaluationContextProvider DEFAULT_CONTEXT_PROVIDER = createEvaluationContextProvider( |
||||
Collections.emptyList()); |
||||
|
||||
static final StandardEnvironment ENVIRONMENT = new StandardEnvironment(); |
||||
|
||||
private final @Nullable Environment environment; |
||||
private final EvaluationContextProvider evaluationContextProvider; |
||||
|
||||
/** |
||||
* Creates a new {@link QueryMethodValueEvaluationContextAccessor} from {@link ApplicationContext}. |
||||
* |
||||
* @param context the application context to use, must not be {@literal null}. |
||||
*/ |
||||
public QueryMethodValueEvaluationContextAccessor(ApplicationContext context) { |
||||
|
||||
Assert.notNull(context, "ApplicationContext must not be null"); |
||||
|
||||
this.environment = context.getEnvironment(); |
||||
this.evaluationContextProvider = createEvaluationContextProvider(context); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link QueryMethodValueEvaluationContextAccessor} from {@link Environment} and |
||||
* {@link ListableBeanFactory}. |
||||
* |
||||
* @param environment |
||||
* @param beanFactory the bean factory to use, must not be {@literal null}. |
||||
*/ |
||||
public QueryMethodValueEvaluationContextAccessor(@Nullable Environment environment, ListableBeanFactory beanFactory) { |
||||
this(environment, createEvaluationContextProvider(beanFactory)); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link QueryMethodValueEvaluationContextAccessor} from {@link Environment} and |
||||
* {@link EvaluationContextProvider}. |
||||
* |
||||
* @param environment |
||||
* @param evaluationContextProvider the underlying {@link EvaluationContextProvider} to use, must not be |
||||
* {@literal null}. |
||||
*/ |
||||
public QueryMethodValueEvaluationContextAccessor(@Nullable Environment environment, |
||||
EvaluationContextProvider evaluationContextProvider) { |
||||
|
||||
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null"); |
||||
|
||||
this.environment = environment; |
||||
this.evaluationContextProvider = evaluationContextProvider; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link QueryMethodValueEvaluationContextAccessor} for the given {@link EvaluationContextExtension}s. |
||||
* |
||||
* @param environment |
||||
* @param extensions must not be {@literal null}. |
||||
*/ |
||||
public QueryMethodValueEvaluationContextAccessor(@Nullable Environment environment, |
||||
Collection<? extends ExtensionIdAware> extensions) { |
||||
|
||||
Assert.notNull(extensions, "EvaluationContextExtensions must not be null"); |
||||
|
||||
this.environment = environment; |
||||
this.evaluationContextProvider = createEvaluationContextProvider(extensions); |
||||
} |
||||
|
||||
private static EvaluationContextProvider createEvaluationContextProvider(ListableBeanFactory factory) { |
||||
|
||||
return ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR) |
||||
? new ReactiveExtensionAwareEvaluationContextProvider(factory) |
||||
: new ExtensionAwareEvaluationContextProvider(factory); |
||||
|
||||
} |
||||
|
||||
private static EvaluationContextProvider createEvaluationContextProvider( |
||||
Collection<? extends ExtensionIdAware> extensions) { |
||||
|
||||
return ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR) |
||||
? new ReactiveExtensionAwareEvaluationContextProvider(extensions) |
||||
: new ExtensionAwareEvaluationContextProvider(extensions); |
||||
} |
||||
|
||||
/** |
||||
* Creates a default {@link QueryMethodValueEvaluationContextAccessor} using the |
||||
* {@link org.springframework.core.env.StandardEnvironment} and extension-less |
||||
* {@link org.springframework.data.spel.EvaluationContextProvider}. |
||||
* |
||||
* @return a default {@link ValueExpressionDelegate}. |
||||
*/ |
||||
public static QueryMethodValueEvaluationContextAccessor create() { |
||||
return new QueryMethodValueEvaluationContextAccessor(ENVIRONMENT, DEFAULT_CONTEXT_PROVIDER); |
||||
} |
||||
|
||||
EvaluationContextProvider getEvaluationContextProvider() { |
||||
return evaluationContextProvider; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link ValueEvaluationContextProvider} for the given {@link Parameters}. |
||||
* |
||||
* @param parameters must not be {@literal null}. |
||||
* @return a new {@link ValueEvaluationContextProvider} for the given {@link Parameters}. |
||||
*/ |
||||
public ValueEvaluationContextProvider create(Parameters<?, ?> parameters) { |
||||
|
||||
Assert.notNull(parameters, "Parameters must not be null"); |
||||
|
||||
if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) { |
||||
if (evaluationContextProvider instanceof ReactiveEvaluationContextProvider reactive) { |
||||
return new DefaultReactiveQueryMethodValueEvaluationContextProvider(environment, parameters, reactive); |
||||
} |
||||
} |
||||
|
||||
return new DefaultQueryMethodValueEvaluationContextProvider(environment, parameters, evaluationContextProvider); |
||||
} |
||||
|
||||
/** |
||||
* Exposes variables for all named parameters for the given arguments. Also exposes non-bindable parameters under the |
||||
* names of their types. |
||||
* |
||||
* @param parameters must not be {@literal null}. |
||||
* @param arguments must not be {@literal null}. |
||||
* @return |
||||
*/ |
||||
static Map<String, Object> collectVariables(Parameters<?, ?> parameters, Object[] arguments) { |
||||
|
||||
if (parameters.getNumberOfParameters() != arguments.length) { |
||||
|
||||
throw new IllegalArgumentException( |
||||
"Number of method parameters (%d) must match the number of method invocation arguments (%d)" |
||||
.formatted(parameters.getNumberOfParameters(), arguments.length)); |
||||
} |
||||
|
||||
Map<String, Object> variables = new HashMap<>(parameters.getNumberOfParameters(), 1.0f); |
||||
|
||||
for (Parameter parameter : parameters) { |
||||
|
||||
if (parameter.isSpecialParameter()) { |
||||
variables.put(//
|
||||
StringUtils.uncapitalize(parameter.getType().getSimpleName()), //
|
||||
arguments[parameter.getIndex()]); |
||||
} |
||||
|
||||
if (parameter.isNamedParameter()) { |
||||
variables.put(parameter.getRequiredName(), //
|
||||
arguments[parameter.getIndex()]); |
||||
} |
||||
} |
||||
|
||||
return variables; |
||||
} |
||||
|
||||
/** |
||||
* Imperative {@link ValueEvaluationContextProvider} variant. |
||||
*/ |
||||
static class DefaultQueryMethodValueEvaluationContextProvider implements ValueEvaluationContextProvider { |
||||
|
||||
final @Nullable Environment environment; |
||||
final Parameters<?, ?> parameters; |
||||
final EvaluationContextProvider delegate; |
||||
|
||||
DefaultQueryMethodValueEvaluationContextProvider(@Nullable Environment environment, Parameters<?, ?> parameters, |
||||
EvaluationContextProvider delegate) { |
||||
this.environment = environment; |
||||
this.parameters = parameters; |
||||
this.delegate = delegate; |
||||
} |
||||
|
||||
@Override |
||||
public ValueEvaluationContext getEvaluationContext(@Nullable Object rootObject) { |
||||
return doGetEvaluationContext(delegate.getEvaluationContext(rootObject), rootObject); |
||||
} |
||||
|
||||
@Override |
||||
public ValueEvaluationContext getEvaluationContext(@Nullable Object rootObject, |
||||
ExpressionDependencies dependencies) { |
||||
return doGetEvaluationContext(delegate.getEvaluationContext(rootObject, dependencies), rootObject); |
||||
} |
||||
|
||||
ValueEvaluationContext doGetEvaluationContext(EvaluationContext evaluationContext, @Nullable Object rootObject) { |
||||
|
||||
if (rootObject instanceof Object[] parameterValues) { |
||||
collectVariables(parameters, parameterValues).forEach(evaluationContext::setVariable); |
||||
} |
||||
|
||||
return ValueEvaluationContext.of(environment, evaluationContext); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Reactive {@link ValueEvaluationContextProvider} extension to |
||||
* {@link DefaultQueryMethodValueEvaluationContextProvider}. |
||||
*/ |
||||
static class DefaultReactiveQueryMethodValueEvaluationContextProvider |
||||
extends DefaultQueryMethodValueEvaluationContextProvider implements ReactiveValueEvaluationContextProvider { |
||||
|
||||
private final ReactiveEvaluationContextProvider delegate; |
||||
|
||||
DefaultReactiveQueryMethodValueEvaluationContextProvider(@Nullable Environment environment, |
||||
Parameters<?, ?> parameters, ReactiveEvaluationContextProvider delegate) { |
||||
super(environment, parameters, delegate); |
||||
this.delegate = delegate; |
||||
} |
||||
|
||||
@Override |
||||
public Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject) { |
||||
return delegate.getEvaluationContextLater(rootObject).map(it -> doGetEvaluationContext(it, rootObject)); |
||||
} |
||||
|
||||
@Override |
||||
public Mono<ValueEvaluationContext> getEvaluationContextLater(@Nullable Object rootObject, |
||||
ExpressionDependencies dependencies) { |
||||
return delegate.getEvaluationContextLater(rootObject, dependencies) |
||||
.map(it -> doGetEvaluationContext(it, rootObject)); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
/* |
||||
* Copyright 2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.query; |
||||
|
||||
import org.springframework.data.expression.ValueEvaluationContext; |
||||
import org.springframework.data.expression.ValueEvaluationContextProvider; |
||||
import org.springframework.data.expression.ValueExpression; |
||||
import org.springframework.data.expression.ValueExpressionParser; |
||||
import org.springframework.expression.ParseException; |
||||
|
||||
/** |
||||
* Delegate to provide a {@link ValueExpressionParser} along with a context factory. |
||||
* <p> |
||||
* Subclasses can customize parsing behavior. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
public class ValueExpressionDelegate implements ValueExpressionParser { |
||||
|
||||
private final QueryMethodValueEvaluationContextAccessor contextAccessor; |
||||
private final ValueExpressionParser valueExpressionParser; |
||||
|
||||
/** |
||||
* Creates a new {@link ValueExpressionDelegate} given {@link QueryMethodValueEvaluationContextAccessor} and |
||||
* {@link ValueExpressionParser}. |
||||
* |
||||
* @param contextAccessor the factory to create value evaluation context providers, must not be {@code null}. |
||||
* @param valueExpressionParser the parser to parse expression strings into value expressions, must not be |
||||
* {@code null}. |
||||
*/ |
||||
public ValueExpressionDelegate(QueryMethodValueEvaluationContextAccessor contextAccessor, |
||||
ValueExpressionParser valueExpressionParser) { |
||||
this.contextAccessor = contextAccessor; |
||||
this.valueExpressionParser = valueExpressionParser; |
||||
} |
||||
|
||||
ValueExpressionDelegate(ValueExpressionDelegate original) { |
||||
this.contextAccessor = original.contextAccessor; |
||||
this.valueExpressionParser = original.valueExpressionParser; |
||||
} |
||||
|
||||
/** |
||||
* Creates a default {@link ValueExpressionDelegate} using the |
||||
* {@link org.springframework.core.env.StandardEnvironment}, a default {@link ValueExpression} and extension-less |
||||
* {@link org.springframework.data.spel.EvaluationContextProvider}. |
||||
* |
||||
* @return a default {@link ValueExpressionDelegate}. |
||||
*/ |
||||
public static ValueExpressionDelegate create() { |
||||
return new ValueExpressionDelegate(QueryMethodValueEvaluationContextAccessor.create(), |
||||
ValueExpressionParser.create()); |
||||
} |
||||
|
||||
public ValueExpressionParser getValueExpressionParser() { |
||||
return valueExpressionParser; |
||||
} |
||||
|
||||
public QueryMethodValueEvaluationContextAccessor getEvaluationContextAccessor() { |
||||
return contextAccessor; |
||||
} |
||||
|
||||
/** |
||||
* Creates a {@link ValueEvaluationContextProvider} for query method {@link Parameters} for later creation of a |
||||
* {@link ValueEvaluationContext} based on the actual method parameter values. The resulting |
||||
* {@link ValueEvaluationContextProvider} is only valid for the given parameters |
||||
* |
||||
* @param parameters the query method parameters to use. |
||||
* @return |
||||
*/ |
||||
public ValueEvaluationContextProvider createValueContextProvider(Parameters<?, ?> parameters) { |
||||
return contextAccessor.create(parameters); |
||||
} |
||||
|
||||
@Override |
||||
public ValueExpression parse(String expressionString) throws ParseException { |
||||
return valueExpressionParser.parse(expressionString); |
||||
} |
||||
} |
||||
@ -0,0 +1,459 @@
@@ -0,0 +1,459 @@
|
||||
/* |
||||
* Copyright 2018-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.query; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.function.BiFunction; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import org.springframework.data.domain.Range; |
||||
import org.springframework.data.domain.Range.Bound; |
||||
import org.springframework.data.expression.ValueEvaluationContext; |
||||
import org.springframework.data.expression.ValueEvaluationContextProvider; |
||||
import org.springframework.data.expression.ValueExpression; |
||||
import org.springframework.data.expression.ValueExpressionParser; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* A {@literal ValueExpressionQueryRewriter} is able to detect Value expressions in a query string and to replace them |
||||
* with bind variables. |
||||
* <p> |
||||
* Result of the parse process is a {@link ParsedQuery} which provides the transformed query string. Alternatively and |
||||
* preferred one may provide a {@link QueryMethodValueEvaluationContextAccessor} via |
||||
* {@link #withEvaluationContextAccessor(QueryMethodValueEvaluationContextAccessor)} which will yield the more powerful |
||||
* {@link EvaluatingValueExpressionQueryRewriter}. |
||||
* <p> |
||||
* Typical usage looks like |
||||
* |
||||
* <pre class="code"> |
||||
* ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter |
||||
* .of(valueExpressionParser, (counter, expression) -> String.format("__$synthetic$__%d", counter), String::concat) |
||||
* .withEvaluationContextAccessor(evaluationContextProviderFactory); |
||||
* |
||||
* ValueExpressionQueryRewriter.QueryExpressionEvaluator evaluator = rewriter.parse(query, queryMethod.getParameters()); |
||||
* |
||||
* evaluator.evaluate(objects).forEach(parameterMap::addValue); |
||||
* </pre> |
||||
* |
||||
* @author Jens Schauder |
||||
* @author Gerrit Meier |
||||
* @author Mark Paluch |
||||
* @since 3.3 |
||||
* @see ValueExpression |
||||
*/ |
||||
public class ValueExpressionQueryRewriter { |
||||
|
||||
private static final Pattern EXPRESSION_PATTERN = Pattern.compile("([:?])([#$]\\{[^}]+})"); |
||||
|
||||
private final ValueExpressionParser expressionParser; |
||||
|
||||
/** |
||||
* A function from the index of a Value expression in a query and the actual Value Expression to the parameter name to |
||||
* be used in place of the Value Expression. A typical implementation is expected to look like |
||||
* {@code (index, expression) -> "__some_placeholder_" + index}. |
||||
*/ |
||||
private final BiFunction<Integer, String, String> parameterNameSource; |
||||
|
||||
/** |
||||
* A function from a prefix used to demarcate a Value Expression in a query and a parameter name as returned from |
||||
* {@link #parameterNameSource} to a {@literal String} to be used as a replacement of the Value Expressions in the |
||||
* query. The returned value should normally be interpretable as a bind parameter by the underlying persistence |
||||
* mechanism. A typical implementation is expected to look like {@code (prefix, name) -> prefix + name} or |
||||
* {@code (prefix, name) -> "{" + name + "}"}. |
||||
*/ |
||||
private final BiFunction<String, String, String> replacementSource; |
||||
|
||||
private ValueExpressionQueryRewriter(ValueExpressionParser expressionParser, |
||||
BiFunction<Integer, String, String> parameterNameSource, BiFunction<String, String, String> replacementSource) { |
||||
|
||||
Assert.notNull(expressionParser, "ValueExpressionParser must not be null"); |
||||
Assert.notNull(parameterNameSource, "Parameter name source must not be null"); |
||||
Assert.notNull(replacementSource, "Replacement source must not be null"); |
||||
|
||||
this.parameterNameSource = parameterNameSource; |
||||
this.replacementSource = replacementSource; |
||||
this.expressionParser = expressionParser; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new ValueExpressionQueryRewriter using the given {@link ValueExpressionParser} and rewrite functions. |
||||
* |
||||
* @param expressionParser the expression parser to use. |
||||
* @param parameterNameSource function to generate parameter names. Typically, a function of the form |
||||
* {@code (index, expression) -> "__some_placeholder_" + index}. |
||||
* @param replacementSource function to generate replacements. Typically, a concatenation of the prefix and the |
||||
* parameter name such as {@code String::concat}. |
||||
* @return a ValueExpressionQueryRewriter instance to rewrite queries and extract parsed {@link ValueExpression}s. |
||||
*/ |
||||
public static ValueExpressionQueryRewriter of(ValueExpressionParser expressionParser, |
||||
BiFunction<Integer, String, String> parameterNameSource, BiFunction<String, String, String> replacementSource) { |
||||
return new ValueExpressionQueryRewriter(expressionParser, parameterNameSource, replacementSource); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new EvaluatingValueExpressionQueryRewriter using the given {@link ValueExpressionDelegate} and rewrite |
||||
* functions. |
||||
* |
||||
* @param delegate the ValueExpressionDelegate to use for parsing and to obtain EvaluationContextAccessor from. |
||||
* @param parameterNameSource function to generate parameter names. Typically, a function of the form |
||||
* {@code (index, expression) -> "__some_placeholder_" + index}. |
||||
* @param replacementSource function to generate replacements. Typically, a concatenation of the prefix and the |
||||
* parameter name such as {@code String::concat}. |
||||
* @return a EvaluatingValueExpressionQueryRewriter instance to rewrite queries and extract parsed |
||||
* {@link ValueExpression}s. |
||||
* @since 3.4 |
||||
*/ |
||||
public static EvaluatingValueExpressionQueryRewriter of(ValueExpressionDelegate delegate, |
||||
BiFunction<Integer, String, String> parameterNameSource, BiFunction<String, String, String> replacementSource) { |
||||
return of((ValueExpressionParser) delegate, parameterNameSource, replacementSource) |
||||
.withEvaluationContextAccessor(delegate.getEvaluationContextAccessor()); |
||||
} |
||||
|
||||
/** |
||||
* Parses the query for {@link org.springframework.data.expression.ValueExpression value expressions} using the |
||||
* pattern: |
||||
* |
||||
* <pre> |
||||
* <prefix>#{<spel>} |
||||
* <prefix>${<property placeholder>} |
||||
* </pre> |
||||
* <p> |
||||
* with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double |
||||
* quotation marks. |
||||
* |
||||
* @param query a query containing Value Expressions in the format described above. Must not be {@literal null}. |
||||
* @return A {@link ParsedQuery} which makes the query with Value Expressions replaced by bind parameters and a map |
||||
* from bind parameter to Value Expression available. Guaranteed to be not {@literal null}. |
||||
*/ |
||||
public ParsedQuery parse(String query) { |
||||
return new ParsedQuery(expressionParser, query); |
||||
} |
||||
|
||||
/** |
||||
* Creates a {@link EvaluatingValueExpressionQueryRewriter} from the current one and the given |
||||
* {@link QueryMethodValueEvaluationContextAccessor}. |
||||
* |
||||
* @param accessor must not be {@literal null}. |
||||
* @return EvaluatingValueExpressionQueryRewriter instance to rewrite and evaluate Value Expressions. |
||||
*/ |
||||
public EvaluatingValueExpressionQueryRewriter withEvaluationContextAccessor( |
||||
QueryMethodValueEvaluationContextAccessor accessor) { |
||||
|
||||
Assert.notNull(accessor, "QueryMethodValueEvaluationContextAccessor must not be null"); |
||||
|
||||
return new EvaluatingValueExpressionQueryRewriter(expressionParser, accessor, parameterNameSource, |
||||
replacementSource); |
||||
} |
||||
|
||||
/** |
||||
* An extension of {@link ValueExpressionQueryRewriter} that can create {@link QueryExpressionEvaluator} instances as |
||||
* it also knows about a {@link QueryMethodValueEvaluationContextAccessor}. |
||||
* |
||||
* @author Oliver Gierke |
||||
*/ |
||||
public static class EvaluatingValueExpressionQueryRewriter extends ValueExpressionQueryRewriter { |
||||
|
||||
private final QueryMethodValueEvaluationContextAccessor contextProviderFactory; |
||||
|
||||
/** |
||||
* Creates a new {@link EvaluatingValueExpressionQueryRewriter} for the given |
||||
* {@link QueryMethodValueEvaluationContextAccessor}, parameter name source and replacement source. |
||||
* |
||||
* @param factory must not be {@literal null}. |
||||
* @param parameterNameSource must not be {@literal null}. |
||||
* @param replacementSource must not be {@literal null}. |
||||
*/ |
||||
private EvaluatingValueExpressionQueryRewriter(ValueExpressionParser expressionParser, |
||||
QueryMethodValueEvaluationContextAccessor factory, |
||||
BiFunction<Integer, String, String> parameterNameSource, BiFunction<String, String, String> replacementSource) { |
||||
|
||||
super(expressionParser, parameterNameSource, replacementSource); |
||||
|
||||
this.contextProviderFactory = factory; |
||||
} |
||||
|
||||
/** |
||||
* Parses the query for Value Expressions using the pattern: |
||||
* |
||||
* <pre> |
||||
* <prefix>#{<spel>} |
||||
* <prefix>${<property placeholder>} |
||||
* </pre> |
||||
* <p> |
||||
* with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double |
||||
* quotation marks. |
||||
* |
||||
* @param query a query containing Value Expressions in the format described above. Must not be {@literal null}. |
||||
* @param parameters a {@link Parameters} instance describing query method parameters |
||||
* @return A {@link QueryExpressionEvaluator} which allows to evaluate the Value Expressions. |
||||
*/ |
||||
public QueryExpressionEvaluator parse(String query, Parameters<?, ?> parameters) { |
||||
return new QueryExpressionEvaluator(contextProviderFactory.create(parameters), parse(query)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Parses a query string, identifies the contained Value expressions, replaces them with bind parameters and offers a |
||||
* {@link Map} from those bind parameters to the value expression. |
||||
* <p> |
||||
* The parser detects quoted parts of the query string and does not detect value expressions inside such quoted parts |
||||
* of the query. |
||||
* |
||||
* @author Jens Schauder |
||||
* @author Oliver Gierke |
||||
* @author Mark Paluch |
||||
*/ |
||||
public class ParsedQuery { |
||||
|
||||
private static final int PREFIX_GROUP_INDEX = 1; |
||||
private static final int EXPRESSION_GROUP_INDEX = 2; |
||||
|
||||
private final String query; |
||||
private final Map<String, ValueExpression> expressions; |
||||
private final QuotationMap quotations; |
||||
|
||||
/** |
||||
* Creates a ExpressionDetector from a query String. |
||||
* |
||||
* @param query must not be {@literal null}. |
||||
*/ |
||||
ParsedQuery(ValueExpressionParser parser, String query) { |
||||
|
||||
Assert.notNull(query, "Query must not be null"); |
||||
|
||||
Map<String, ValueExpression> expressions = new HashMap<>(); |
||||
Matcher matcher = EXPRESSION_PATTERN.matcher(query); |
||||
StringBuilder resultQuery = new StringBuilder(); |
||||
QuotationMap quotedAreas = new QuotationMap(query); |
||||
|
||||
int expressionCounter = 0; |
||||
int matchedUntil = 0; |
||||
|
||||
while (matcher.find()) { |
||||
|
||||
if (quotedAreas.isQuoted(matcher.start())) { |
||||
resultQuery.append(query, matchedUntil, matcher.end()); |
||||
|
||||
} else { |
||||
|
||||
String expressionString = matcher.group(EXPRESSION_GROUP_INDEX); |
||||
String prefix = matcher.group(PREFIX_GROUP_INDEX); |
||||
|
||||
String parameterName = parameterNameSource.apply(expressionCounter, expressionString); |
||||
String replacement = replacementSource.apply(prefix, parameterName); |
||||
|
||||
resultQuery.append(query, matchedUntil, matcher.start()); |
||||
resultQuery.append(replacement); |
||||
|
||||
expressions.put(parameterName, parser.parse(expressionString)); |
||||
expressionCounter++; |
||||
} |
||||
|
||||
matchedUntil = matcher.end(); |
||||
} |
||||
|
||||
resultQuery.append(query.substring(matchedUntil)); |
||||
|
||||
this.expressions = Collections.unmodifiableMap(expressions); |
||||
this.query = resultQuery.toString(); |
||||
|
||||
// recreate quotation map based on rewritten query.
|
||||
this.quotations = new QuotationMap(this.query); |
||||
} |
||||
|
||||
/** |
||||
* The query with all the Value Expressions replaced with bind parameters. |
||||
* |
||||
* @return Guaranteed to be not {@literal null}. |
||||
*/ |
||||
public String getQueryString() { |
||||
return query; |
||||
} |
||||
|
||||
/** |
||||
* Return whether the {@link #getQueryString() query} at {@code index} is quoted. |
||||
* |
||||
* @param index |
||||
* @return {@literal true} if quoted; {@literal false} otherwise. |
||||
*/ |
||||
public boolean isQuoted(int index) { |
||||
return quotations.isQuoted(index); |
||||
} |
||||
|
||||
public ValueExpression getParameter(String name) { |
||||
return expressions.get(name); |
||||
} |
||||
|
||||
/** |
||||
* Returns the number of expressions in this extractor. |
||||
* |
||||
* @return the number of expressions in this extractor. |
||||
*/ |
||||
public int size() { |
||||
return expressions.size(); |
||||
} |
||||
|
||||
/** |
||||
* Returns whether the query contains Value Expressions. |
||||
* |
||||
* @return {@literal true} if the query contains Value Expressions. |
||||
*/ |
||||
public boolean hasParameterBindings() { |
||||
return !expressions.isEmpty(); |
||||
} |
||||
|
||||
/** |
||||
* A {@literal Map} from parameter name to Value Expression. |
||||
* |
||||
* @return Guaranteed to be not {@literal null}. |
||||
*/ |
||||
public Map<String, ValueExpression> getParameterMap() { |
||||
return expressions; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers an |
||||
* API to query that information. |
||||
* |
||||
* @author Jens Schauder |
||||
* @author Oliver Gierke |
||||
* @since 2.1 |
||||
*/ |
||||
static class QuotationMap { |
||||
|
||||
private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\''); |
||||
|
||||
private final List<Range<Integer>> quotedRanges = new ArrayList<>(); |
||||
|
||||
/** |
||||
* Creates a new {@link QuotationMap} for the query. |
||||
* |
||||
* @param query can be {@literal null}. |
||||
*/ |
||||
public QuotationMap(@Nullable String query) { |
||||
|
||||
if (query == null) { |
||||
return; |
||||
} |
||||
|
||||
Character inQuotation = null; |
||||
int start = 0; |
||||
|
||||
for (int i = 0; i < query.length(); i++) { |
||||
|
||||
char currentChar = query.charAt(i); |
||||
|
||||
if (QUOTING_CHARACTERS.contains(currentChar)) { |
||||
|
||||
if (inQuotation == null) { |
||||
|
||||
inQuotation = currentChar; |
||||
start = i; |
||||
|
||||
} else if (currentChar == inQuotation) { |
||||
|
||||
inQuotation = null; |
||||
|
||||
quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i))); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (inQuotation != null) { |
||||
throw new IllegalArgumentException( |
||||
String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Checks if a given index is within a quoted range. |
||||
* |
||||
* @param index to check if it is part of a quoted range. |
||||
* @return whether the query contains a quoted range at {@literal index}. |
||||
*/ |
||||
public boolean isQuoted(int index) { |
||||
return quotedRanges.stream().anyMatch(r -> r.contains(index)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Evaluates Value expressions as detected by {@link ParsedQuery} based on parameter information from a method and |
||||
* parameter values from a method call. |
||||
* |
||||
* @author Jens Schauder |
||||
* @author Gerrit Meier |
||||
* @author Oliver Gierke |
||||
* @see ValueExpressionQueryRewriter#parse(String) |
||||
*/ |
||||
public class QueryExpressionEvaluator { |
||||
|
||||
private final ValueEvaluationContextProvider evaluationContextProvider; |
||||
private final ParsedQuery detector; |
||||
|
||||
public QueryExpressionEvaluator(ValueEvaluationContextProvider evaluationContextProvider, |
||||
ParsedQuery detector) { |
||||
this.evaluationContextProvider = evaluationContextProvider; |
||||
this.detector = detector; |
||||
} |
||||
|
||||
/** |
||||
* Evaluate all value expressions in {@link ParsedQuery} based on values provided as an argument. |
||||
* |
||||
* @param values Parameter values. Must not be {@literal null}. |
||||
* @return a map from parameter name to evaluated value as of {@link ParsedQuery#getParameterMap()}. |
||||
*/ |
||||
public Map<String, Object> evaluate(Object[] values) { |
||||
|
||||
Assert.notNull(values, "Values must not be null."); |
||||
|
||||
Map<String, ValueExpression> parameterMap = detector.getParameterMap(); |
||||
Map<String, Object> results = new LinkedHashMap<>(parameterMap.size()); |
||||
|
||||
parameterMap.forEach((parameter, expression) -> results.put(parameter, evaluate(expression, values))); |
||||
|
||||
return results; |
||||
} |
||||
|
||||
/** |
||||
* Returns the query string produced by the intermediate Value Expression collection step. |
||||
* |
||||
* @return |
||||
*/ |
||||
public String getQueryString() { |
||||
return detector.getQueryString(); |
||||
} |
||||
|
||||
@Nullable |
||||
private Object evaluate(ValueExpression expression, Object[] values) { |
||||
|
||||
ValueEvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(values, |
||||
expression.getExpressionDependencies()); |
||||
|
||||
return expression.evaluate(evaluationContext); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,160 @@
@@ -0,0 +1,160 @@
|
||||
/* |
||||
* Copyright 2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.query; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.Arrays; |
||||
import java.util.Map; |
||||
import java.util.function.BiFunction; |
||||
|
||||
import org.assertj.core.groups.Tuple; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.core.env.MapPropertySource; |
||||
import org.springframework.core.env.StandardEnvironment; |
||||
import org.springframework.data.expression.ValueExpressionParser; |
||||
import org.springframework.data.spel.EvaluationContextProvider; |
||||
|
||||
/** |
||||
* Unit tests for {@link ValueExpressionQueryRewriter}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class ValueExpressionQueryRewriterUnitTests { |
||||
|
||||
static final BiFunction<Integer, String, String> PARAMETER_NAME_SOURCE = (index, spel) -> "EPP" + index; |
||||
static final BiFunction<String, String, String> REPLACEMENT_SOURCE = (prefix, name) -> prefix + name; |
||||
static final ValueExpressionParser PARSER = ValueExpressionParser.create(); |
||||
|
||||
@Test // GH-3049
|
||||
void nullQueryThrowsException() { |
||||
|
||||
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); |
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> context.parse(null)); |
||||
} |
||||
|
||||
@Test // GH-3049
|
||||
void emptyStringGetsParsedCorrectly() { |
||||
|
||||
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); |
||||
var extractor = context.parse(""); |
||||
|
||||
assertThat(extractor.getQueryString()).isEqualTo(""); |
||||
assertThat(extractor.getParameterMap()).isEmpty(); |
||||
} |
||||
|
||||
@Test // GH-3049
|
||||
void findsAndReplacesExpressions() { |
||||
|
||||
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); |
||||
var extractor = context.parse(":#{one} ?#{two} :${three} ?${four}"); |
||||
|
||||
assertThat(extractor.getQueryString()).isEqualTo(":EPP0 ?EPP1 :EPP2 ?EPP3"); |
||||
assertThat(extractor.getParameterMap().entrySet()) //
|
||||
.extracting(Map.Entry::getKey, it -> it.getValue().getExpressionString()) //
|
||||
.containsExactlyInAnyOrder( //
|
||||
Tuple.tuple("EPP0", "one"), //
|
||||
Tuple.tuple("EPP1", "two"), //
|
||||
Tuple.tuple("EPP2", "${three}"), //
|
||||
Tuple.tuple("EPP3", "${four}") //
|
||||
); |
||||
} |
||||
|
||||
@Test // GH-3049
|
||||
void keepsStringWhenNoMatchIsFound() { |
||||
|
||||
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); |
||||
var extractor = context.parse("abcdef"); |
||||
|
||||
assertThat(extractor.getQueryString()).isEqualTo("abcdef"); |
||||
assertThat(extractor.getParameterMap()).isEmpty(); |
||||
} |
||||
|
||||
@Test // GH-3049
|
||||
void spelsInQuotesGetIgnored() { |
||||
|
||||
var queries = Arrays.asList(//
|
||||
"a'b:#{one}cd'ef", //
|
||||
"a'b:#{o'ne}cdef", //
|
||||
"ab':#{one}'cdef", //
|
||||
"ab:'#{one}cd'ef", //
|
||||
"ab:#'{one}cd'ef", //
|
||||
"a'b:#{o'ne}cdef"); |
||||
|
||||
queries.forEach(this::checkNoExpressionIsFound); |
||||
} |
||||
|
||||
private void checkNoExpressionIsFound(String query) { |
||||
|
||||
var context = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); |
||||
var extractor = context.parse(query); |
||||
|
||||
assertThat(extractor.getQueryString()).describedAs(query).isEqualTo(query); |
||||
assertThat(extractor.getParameterMap()).describedAs(query).isEmpty(); |
||||
} |
||||
|
||||
@Test // GH-3049
|
||||
void shouldEvaluateExpression() throws Exception { |
||||
|
||||
StandardEnvironment environment = new StandardEnvironment(); |
||||
environment.getPropertySources().addFirst(new MapPropertySource("synthetic", Map.of("foo", "world"))); |
||||
|
||||
QueryMethodValueEvaluationContextAccessor contextAccessor = new QueryMethodValueEvaluationContextAccessor( |
||||
environment, |
||||
EvaluationContextProvider.DEFAULT); |
||||
|
||||
ValueExpressionDelegate delegate = new ValueExpressionDelegate(contextAccessor, PARSER); |
||||
ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter |
||||
.of(delegate, PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); |
||||
|
||||
Method method = ValueExpressionQueryRewriterUnitTests.MyRepository.class.getDeclaredMethod("simpleExpression", |
||||
String.class); |
||||
var extractor = rewriter.parse("SELECT :#{#value}, :${foo}", |
||||
new DefaultParameters(ParametersSource.of(method))); |
||||
|
||||
assertThat(extractor.getQueryString()).isEqualTo("SELECT :EPP0, :EPP1"); |
||||
assertThat(extractor.evaluate(new Object[] { "hello" })).containsEntry("EPP0", "hello").containsEntry("EPP1", |
||||
"world"); |
||||
} |
||||
|
||||
@Test // GH-3049
|
||||
void shouldAllowNullValues() throws Exception { |
||||
|
||||
ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(PARSER, PARAMETER_NAME_SOURCE, |
||||
REPLACEMENT_SOURCE); |
||||
StandardEnvironment environment = new StandardEnvironment(); |
||||
|
||||
QueryMethodValueEvaluationContextAccessor factory = new QueryMethodValueEvaluationContextAccessor(environment, |
||||
EvaluationContextProvider.DEFAULT); |
||||
|
||||
Method method = ValueExpressionQueryRewriterUnitTests.MyRepository.class.getDeclaredMethod("simpleExpression", |
||||
String.class); |
||||
var extractor = rewriter.withEvaluationContextAccessor(factory).parse("SELECT :#{#value}", |
||||
new DefaultParameters(ParametersSource.of(method))); |
||||
|
||||
assertThat(extractor.getQueryString()).isEqualTo("SELECT :EPP0"); |
||||
assertThat(extractor.evaluate(new Object[] { null })).containsEntry("EPP0", null); |
||||
} |
||||
|
||||
interface MyRepository { |
||||
|
||||
void simpleExpression(String value); |
||||
|
||||
} |
||||
} |
||||
Loading…
Reference in new issue