Evolve ValueExpressionParser.

Introduce literal, expression and placeholder variants. Add parser for composite expressions.

Closes #2369
Original pull request: #3036
This commit is contained in:
Mark Paluch
2024-01-29 15:45:07 +01:00
parent 9f13e5450d
commit 67163d02e3
40 changed files with 1650 additions and 911 deletions

View File

@@ -15,6 +15,7 @@
** xref:repositories/null-handling.adoc[]
** xref:repositories/projections.adoc[]
* xref:query-by-example.adoc[]
* xref:value-expressions.adoc[]
* xref:auditing.adoc[]
* xref:custom-conversions.adoc[]
* xref:entity-callbacks.adoc[]

View File

@@ -0,0 +1,246 @@
[[valueexpressions.fundamentals]]
= Value Expressions Fundamentals
Value Expressions are a combination of {spring-framework-docs}/core/expressions.html[Spring Expression Language (SpEL)] and {spring-framework-docs}/core/beans/environment.html#beans-placeholder-resolution-in-statements[Property Placeholder Resolution].
They combine powerful evaluation of programmatic expressions with the simplicity to resort to property-placeholder resolution to obtain values from the `Environment` such as configuration properties.
Expressions are expected to be defined by a trusted input such as an annotation value and not to be determined from user input.
The following code demonstrates how to use expressions in the context of annotations.
.Annotation Usage
====
[source,java]
----
@Document("orders-#{tenantService.getOrderCollection()}-${tenant-config.suffix}")
class Order {
// …
}
----
====
Value Expressions can be defined from a sole SpEL Expression, a Property Placeholder or a composite expression mixing various expressions including literals.
.Expression Examples
====
[source]
----
#{tenantService.getOrderCollection()} <1>
#{(1+1) + '-hello-world'} <2>
${tenant-config.suffix} <3>
orders-${tenant-config.suffix} <4>
#{tenantService.getOrderCollection()}-${tenant-config.suffix} <5>
----
<1> Value Expression using a single SpEL Expression.
<2> Value Expression using a static SpEL Expression evaluating to `2-hello-world`.
<3> Value Expression using a single Property Placeholder.
<4> Composite expression comprised of the literal `orders-` and the Property Placeholder `${tenant-config.suffix}`.
<5> Composite expression using SpEL, Property Placeholders and literals.
====
NOTE: Using value expressions introduces a lot of flexibility to your code.
Doing so requires evaluation of the expression on each usage and, therefore, value expression evaluation has an impact on the performance profile.
[[valueexpressions.api]]
== Parsing and Evaluation
Value Expressions are parsed by the `ValueExpressionParser` API.
Instances of `ValueExpression` are thread-safe and can be cached for later use to avoid repeated parsing.
The following example shows the Value Expression API usage:
.Parsing and Evaluation
[tabs]
======
Java::
+
[source,java,role="primary"]
----
ValueParserConfiguration configuration = SpelExpressionParser::new;
ValueEvaluationContext context = ValueEvaluationContext.of(environment, evaluationContext);
ValueExpressionParser parser = ValueExpressionParser.create(configuration);
ValueExpression expression = parser.parse("Hello, World");
Object result = expression.evaluate(context);
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
val configuration = ValueParserConfiguration { SpelExpressionParser() }
val context = ValueEvaluationContext.of(environment, evaluationContext)
val parser = ValueExpressionParser.create(configuration)
val expression: ValueExpression = parser.parse("Hello, World")
val result: Any = expression.evaluate(context)
----
======
[[valueexpressions.spel]]
== SpEL Expressions
{spring-framework-docs}/core/expressions.html[SpEL Expressions] follow the Template style where the expression is expected to be enclosed within the `#{…}` format.
Expressions are evaluated using an `EvaluationContext` that is provided by `EvaluationContextProvider`.
The context itself is a powerful `StandardEvaluationContext` allowing a wide range of operations, access to static types and context extensions.
NOTE: Make sure to parse and evaluate only expressions from trusted sources such as annotations.
Accepting user-provided expressions can create an entry path to exploit the application context and your system resulting in a potential security vulnerability.
=== Extending the Evaluation Context
`EvaluationContextProvider` and its reactive variant `ReactiveEvaluationContextProvider` provide access to an `EvaluationContext`.
`ExtensionAwareEvaluationContextProvider` and its reactive variant `ReactiveExtensionAwareEvaluationContextProvider` are default implementations that determine context extensions from an application context, specifically `ListableBeanFactory`.
Extensions implement either `EvaluationContextExtension` or `ReactiveEvaluationContextExtension` to provide extension support to hydrate `EvaluationContext`.
That are a root object, properties and functions (top-level methods).
The following example shows a context extension that provides a root object, properties, functions and an aliased function.
.Implementing a `EvaluationContextExtension`
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Component
public class MyExtension implements EvaluationContextExtension {
@Override
public String getExtensionId() {
return "my-extension";
}
@Override
public Object getRootObject() {
return new CustomExtensionRootObject();
}
@Override
public Map<String, Object> getProperties() {
Map<String, Object> properties = new HashMap<>();
properties.put("key", "Hello");
return properties;
}
@Override
public Map<String, Function> getFunctions() {
Map<String, Function> functions = new HashMap<>();
try {
functions.put("aliasedMethod", new Function(getClass().getMethod("extensionMethod")));
return functions;
} catch (Exception o_O) {
throw new RuntimeException(o_O);
}
}
public static String extensionMethod() {
return "Hello World";
}
public static int add(int i1, int i2) {
return i1 + i2;
}
}
public class CustomExtensionRootObject {
public boolean rootObjectInstanceMethod() {
return true;
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Component
class MyExtension : EvaluationContextExtension {
override fun getExtensionId(): String {
return "my-extension"
}
override fun getRootObject(): Any? {
return CustomExtensionRootObject()
}
override fun getProperties(): Map<String, Any> {
val properties: MutableMap<String, Any> = HashMap()
properties["key"] = "Hello"
return properties
}
override fun getFunctions(): Map<String, Function> {
val functions: MutableMap<String, Function> = HashMap()
try {
functions["aliasedMethod"] = Function(javaClass.getMethod("extensionMethod"))
return functions
} catch (o_O: Exception) {
throw RuntimeException(o_O)
}
}
companion object {
fun extensionMethod(): String {
return "Hello World"
}
fun add(i1: Int, i2: Int): Int {
return i1 + i2
}
}
}
class CustomExtensionRootObject {
fun rootObjectInstanceMethod(): Boolean {
return true
}
}
----
======
Once the above shown extension is registered, you can use its exported methods, properties and root object to evaluate SpEL expressions:
.Expression Evaluation Examples
====
[source]
----
#{add(1, 2)} <1>
#{extensionMethod()} <2>
#{aliasedMethod()} <3>
#{key} <4>
#{rootObjectInstanceMethod()} <5>
----
<1> Invoke the method `add` declared by `MyExtension` resulting in `3` as the method adds both numeric parameters and returns the sum.
<2> Invoke the method `extensionMethod` declared by `MyExtension` resulting in `Hello World`.
<3> Invoke the method `aliasedMethod`.
The method is exposed as function and redirects into the method `extensionMethod` declared by `MyExtension` resulting in `Hello World`.
<4> Evaluate the `key` property resulting in `Hello`.
<5> Invoke the method `rootObjectInstanceMethod` on the root object instance `CustomExtensionRootObject`.
====
You can find real-life context extensions at https://github.com/spring-projects/spring-security/blob/main/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java[`SecurityEvaluationContextExtension`].
[[valueexpressions.property-placeholders]]
== Property Placeholders
Property placeholders following the form `${…}` refer to properties provided typically by a `PropertySource` through `Environment`.
Properties are useful to resolve against system properties, application configuration files, environment configuration or property sources contributed by secret management systems.
You can find more details on the property placeholders in {spring-framework-docs}/core/beans/annotation-config/value-annotations.html#page-title[Spring Framework's documentation on `@Value` usage].

View File

@@ -0,0 +1,75 @@
/*
* 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 java.util.List;
import org.springframework.data.spel.ExpressionDependencies;
/**
* Composite {@link ValueExpression} consisting of multiple placeholder, SpEL, and literal expressions.
*
* @param raw
* @param expressions
* @author Mark Paluch
* @since 3.3
*/
record CompositeValueExpression(String raw, List<ValueExpression> expressions) implements ValueExpression {
@Override
public String getExpressionString() {
return raw;
}
@Override
public ExpressionDependencies getExpressionDependencies() {
ExpressionDependencies dependencies = ExpressionDependencies.none();
for (ValueExpression expression : expressions) {
ExpressionDependencies dependency = expression.getExpressionDependencies();
if (!dependency.equals(ExpressionDependencies.none())) {
dependencies = dependencies.mergeWith(dependency);
}
}
return dependencies;
}
@Override
public boolean isLiteral() {
for (ValueExpression expression : expressions) {
if (!expression.isLiteral()) {
return false;
}
}
return true;
}
@Override
public String evaluate(ValueEvaluationContext context) {
StringBuilder builder = new StringBuilder();
for (ValueExpression expression : expressions) {
builder.append((String) expression.evaluate(context));
}
return builder.toString();
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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 org.springframework.core.env.Environment;
import org.springframework.expression.EvaluationContext;
/**
* Default {@link ValueEvaluationContext}.
*
* @author Mark Paluch
* @since 3.3
*/
record DefaultValueEvaluationContext(Environment environment,
EvaluationContext evaluationContext) implements ValueEvaluationContext {
@Override
public Environment getEnvironment() {
return environment();
}
@Override
public EvaluationContext getEvaluationContext() {
return evaluationContext();
}
}

View File

@@ -0,0 +1,180 @@
/*
* 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 java.util.ArrayList;
import java.util.List;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
import org.springframework.util.Assert;
import org.springframework.util.SystemPropertyUtils;
/**
* Default {@link ValueExpressionParser} implementation. Instances are thread-safe.
*
* @author Mark Paluch
* @since 3.3
*/
class DefaultValueExpressionParser implements ValueExpressionParser {
public static final String PLACEHOLDER_PREFIX = SystemPropertyUtils.PLACEHOLDER_PREFIX;
public static final String EXPRESSION_PREFIX = ParserContext.TEMPLATE_EXPRESSION.getExpressionPrefix();
public static final char SUFFIX = '}';
public static final int PLACEHOLDER_PREFIX_LENGTH = PLACEHOLDER_PREFIX.length();
public static final char[] QUOTE_CHARS = { '\'', '"' };
private final ValueParserConfiguration configuration;
public DefaultValueExpressionParser(ValueParserConfiguration configuration) {
Assert.notNull(configuration, "ValueParserConfiguration must not be null");
this.configuration = configuration;
}
@Override
public ValueExpression parse(String expressionString) {
int placerholderIndex = expressionString.indexOf(PLACEHOLDER_PREFIX);
int expressionIndex = expressionString.indexOf(EXPRESSION_PREFIX);
if (placerholderIndex == -1 && expressionIndex == -1) {
return new LiteralValueExpression(expressionString);
}
if (placerholderIndex != -1 && expressionIndex == -1
&& findPlaceholderEndIndex(expressionString, placerholderIndex) != expressionString.length()) {
return createPlaceholder(expressionString);
}
if (placerholderIndex == -1
&& findPlaceholderEndIndex(expressionString, expressionIndex) != expressionString.length()) {
return createExpression(expressionString);
}
return parseComposite(expressionString, placerholderIndex, expressionIndex);
}
private CompositeValueExpression parseComposite(String expressionString, int placerholderIndex, int expressionIndex) {
List<ValueExpression> expressions = new ArrayList<>(PLACEHOLDER_PREFIX_LENGTH);
int startIndex = getStartIndex(placerholderIndex, expressionIndex);
if (startIndex != 0) {
expressions.add(new LiteralValueExpression(expressionString.substring(0, startIndex)));
}
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(expressionString, startIndex);
if (endIndex == -1) {
throw new ParseException(expressionString, startIndex,
"No ending suffix '}' for expression starting at character %d: %s".formatted(startIndex,
expressionString.substring(startIndex)));
}
int afterClosingParenthesisIndex = endIndex + 1;
String part = expressionString.substring(startIndex, afterClosingParenthesisIndex);
if (part.startsWith(PLACEHOLDER_PREFIX)) {
expressions.add(createPlaceholder(part));
} else {
expressions.add(createExpression(part));
}
placerholderIndex = expressionString.indexOf(PLACEHOLDER_PREFIX, endIndex);
expressionIndex = expressionString.indexOf(EXPRESSION_PREFIX, endIndex);
startIndex = getStartIndex(placerholderIndex, expressionIndex);
if (startIndex == -1) {
// no next expression but we're capturing everything after the expression as literal.
expressions.add(new LiteralValueExpression(expressionString.substring(afterClosingParenthesisIndex)));
} else {
// capture literal after the expression ends and before the next starts.
expressions
.add(new LiteralValueExpression(expressionString.substring(afterClosingParenthesisIndex, startIndex)));
}
}
return new CompositeValueExpression(expressionString, expressions);
}
private static int getStartIndex(int placerholderIndex, int expressionIndex) {
return placerholderIndex != -1 && expressionIndex != -1 ? Math.min(placerholderIndex, expressionIndex)
: placerholderIndex != -1 ? placerholderIndex : expressionIndex;
}
private PlaceholderExpression createPlaceholder(String part) {
return new PlaceholderExpression(part);
}
private ExpressionExpression createExpression(String expression) {
Expression expr = configuration.getExpressionParser().parseExpression(expression,
ParserContext.TEMPLATE_EXPRESSION);
ExpressionDependencies dependencies = ExpressionDependencies.discover(expr);
return new ExpressionExpression(expr, dependencies);
}
private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + PLACEHOLDER_PREFIX_LENGTH;
char quotationChar = 0;
char nestingLevel = 0;
boolean skipEscape = false;
while (index < buf.length()) {
char c = buf.charAt(index);
if (!skipEscape && c == '\\') {
skipEscape = true;
} else if (skipEscape) {
skipEscape = false;
} else if (quotationChar == 0) {
for (char quoteChar : QUOTE_CHARS) {
if (quoteChar == c) {
quotationChar = c;
break;
}
}
} else if (quotationChar == c) {
quotationChar = 0;
}
if (!skipEscape && quotationChar == 0) {
if (nestingLevel != 0 && c == SUFFIX) {
nestingLevel--;
} else if (c == '{') {
nestingLevel++;
} else if (nestingLevel == 0 && c == SUFFIX) {
return index;
}
}
index++;
}
return -1;
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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 org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
/**
* SpEL expression.
*
* @param expression
* @author Mark Paluch
* @since 3.3
*/
record ExpressionExpression(Expression expression, ExpressionDependencies dependencies) implements ValueExpression {
@Override
public String getExpressionString() {
return expression.getExpressionString();
}
@Override
public ExpressionDependencies getExpressionDependencies() {
return dependencies();
}
@Override
public boolean isLiteral() {
return false;
}
@Override
public Object evaluate(ValueEvaluationContext context) {
EvaluationContext evaluationContext = context.getEvaluationContext();
if (evaluationContext != null) {
return expression.getValue(evaluationContext);
}
return expression.getValue();
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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;
/**
* Literal expression returning the underlying expression string upon evaluation.
*
* @param expression
* @author Mark Paluch
* @since 3.3
*/
record LiteralValueExpression(String expression) implements ValueExpression {
@Override
public String getExpressionString() {
return expression;
}
@Override
public boolean isLiteral() {
return true;
}
@Override
public String evaluate(ValueEvaluationContext context) {
return expression;
}
}

View File

@@ -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 org.springframework.core.env.Environment;
import org.springframework.expression.EvaluationException;
/**
* Property placeholder expression evaluated against a {@link Environment}.
*
* @param expression
* @author Mark Paluch
* @since 3.3
*/
record PlaceholderExpression(String expression) implements ValueExpression {
@Override
public String getExpressionString() {
return expression;
}
@Override
public boolean isLiteral() {
return false;
}
@Override
public Object evaluate(ValueEvaluationContext context) {
Environment environment = context.getEnvironment();
if (environment != null) {
try {
return environment.resolveRequiredPlaceholders(expression);
} catch (IllegalArgumentException e) {
throw new EvaluationException(e.getMessage(), e);
}
}
return expression;
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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 org.springframework.core.env.Environment;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
/**
* Expressions are executed in an evaluation context. It is in this context that references are resolved when
* encountered during expression evaluation.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.3
*/
public interface ValueEvaluationContext {
/**
* Returns a new {@link ValueEvaluationContext}.
*
* @param environment
* @param evaluationContext
* @return a new {@link ValueEvaluationContext} for the given environment and evaluation context.
*/
static ValueEvaluationContext of(Environment environment, EvaluationContext evaluationContext) {
return new DefaultValueEvaluationContext(environment, evaluationContext);
}
/**
* Returns the {@link Environment} if provided.
*
* @return the {@link Environment} or {@literal null}.
*/
@Nullable
Environment getEnvironment();
/**
* Returns the {@link EvaluationContext} if provided.
*
* @return the {@link EvaluationContext} or {@literal null}.
*/
@Nullable
EvaluationContext getEvaluationContext();
}

View File

@@ -0,0 +1,50 @@
/*
* 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 org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
/**
* SPI to provide to access a centrally defined potentially shared {@link ValueEvaluationContext}.
*
* @author Mark Paluch
* @since 3.3
*/
public interface ValueEvaluationContextProvider {
/**
* Return a {@link EvaluationContext} built using the given parameter values.
*
* @param rootObject the root object to set in the {@link EvaluationContext}.
* @return
*/
ValueEvaluationContext getEvaluationContext(Object rootObject);
/**
* Return a tailored {@link EvaluationContext} built using the given parameter values and considering
* {@link ExpressionDependencies expression dependencies}. The returned {@link EvaluationContext} 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 EvaluationContext}.
* @param dependencies the requested expression dependencies to be available.
* @return
*/
default ValueEvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return getEvaluationContext(rootObject);
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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 org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationException;
import org.springframework.lang.Nullable;
/**
* An expression capable of evaluating itself against context objects. Encapsulates the details of a previously parsed
* expression string. Provides a common abstraction for expression evaluation.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.3
*/
public interface ValueExpression {
/**
* Returns the original string used to create this expression (unmodified).
*
* @return the original expression string.
*/
String getExpressionString();
/**
* Returns the expression dependencies.
*
* @return the dependencies the underlying expression requires. Can be {@link ExpressionDependencies#none()}.
*/
default ExpressionDependencies getExpressionDependencies() {
return ExpressionDependencies.none();
}
/**
* Returns whether the expression is a literal expression (that doesn't actually require evaluation).
*
* @return {@code true} if the expression is a literal expression; {@code false} if the expression can yield a
* different result upon {@link #evaluate(ValueEvaluationContext) evaluation}.
*/
boolean isLiteral();
/**
* Evaluates this expression using the given evaluation context.
*
* @return the evaluation result.
* @throws EvaluationException if there is a problem during evaluation
*/
@Nullable
Object evaluate(ValueEvaluationContext context) throws EvaluationException;
}

View File

@@ -0,0 +1,58 @@
/*
* 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 org.springframework.expression.ParseException;
/**
* Parses expression strings into expressions that can be evaluated. Supports parsing expression, configuration
* templates as well as literal strings.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.3
*/
public interface ValueExpressionParser {
/**
* Creates a new parser to parse expression strings.
*
* @param configuration the parser context configuration.
* @return the parser instance.
*/
static ValueExpressionParser create(ValueParserConfiguration configuration) {
return new DefaultValueExpressionParser(configuration);
}
/**
* Parses the expression string and return an Expression object you can use for repeated evaluation.
* <p>
* Some examples:
*
* <pre class="code">
* #{3 + 4}
* #{name.firstName}
* ${key.one}
* #{name.lastName}-${key.one}
* </pre>
*
* @param expressionString the raw expression string to parse.
* @return an evaluator for the parsed expression.
* @throws ParseException an exception occurred during parsing.
*/
ValueExpression parse(String expressionString) throws ParseException;
}

View File

@@ -0,0 +1,36 @@
/*
* 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 org.springframework.expression.ExpressionParser;
/**
* Configuration for {@link ValueExpressionParser}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.3
*/
public interface ValueParserConfiguration {
/**
* Parser for {@link org.springframework.expression.Expression SpEL expressions}.
*
* @return for {@link org.springframework.expression.Expression SpEL expressions}.
*/
ExpressionParser getExpressionParser();
}

View File

@@ -0,0 +1,5 @@
/**
* Value Expression implementation.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.expression;

View File

@@ -39,11 +39,11 @@ public class Parameter<T, P extends PersistentProperty<P>> {
private final @Nullable String name;
private final TypeInformation<T> type;
private final MergedAnnotations annotations;
private final String key;
private final @Nullable String expression;
private final @Nullable PersistentEntity<T, P> entity;
private final Lazy<Boolean> enclosingClassCache;
private final Lazy<Boolean> hasSpelExpression;
private final Lazy<Boolean> hasExpression;
/**
* Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of
@@ -64,7 +64,7 @@ public class Parameter<T, P extends PersistentProperty<P>> {
this.name = name;
this.type = type;
this.annotations = MergedAnnotations.from(annotations);
this.key = getValue(this.annotations);
this.expression = getValue(this.annotations);
this.entity = entity;
this.enclosingClassCache = Lazy.of(() -> {
@@ -77,7 +77,7 @@ public class Parameter<T, P extends PersistentProperty<P>> {
return ClassUtils.isInnerClass(owningType) && type.getType().equals(owningType.getEnclosingClass());
});
this.hasSpelExpression = Lazy.of(() -> StringUtils.hasText(getSpelExpression()));
this.hasExpression = Lazy.of(() -> StringUtils.hasText(getValueExpression()));
}
@Nullable
@@ -128,21 +128,62 @@ public class Parameter<T, P extends PersistentProperty<P>> {
}
/**
* Returns the key to be used when looking up a source data structure to populate the actual parameter value.
* Returns the expression to be used when looking up a source data structure to populate the actual parameter value.
*
* @return
* @return the expression to be used when looking up a source data structure.
* @deprecated since 3.3, use {@link #getValueExpression()} instead.
*/
@Nullable
public String getSpelExpression() {
return key;
return getValueExpression();
}
/**
* Returns the expression to be used when looking up a source data structure to populate the actual parameter value.
*
* @return the expression to be used when looking up a source data structure.
* @since 3.3
*/
@Nullable
public String getValueExpression() {
return expression;
}
/**
* Returns the required expression to be used when looking up a source data structure to populate the actual parameter
* value or throws {@link IllegalStateException} if there's no expression.
*
* @return the expression to be used when looking up a source data structure.
* @since 3.3
*/
public String getRequiredValueExpression() {
if (!hasValueExpression()) {
throw new IllegalStateException("No expression associated with this parameter");
}
return getValueExpression();
}
/**
* Returns whether the constructor parameter is equipped with a SpEL expression.
*
* @return
* @return {@literal true}} if the parameter is equipped with a SpEL expression.
* @deprecated since 3.3, use {@link #hasValueExpression()} instead.
*/
@Deprecated(since = "3.3")
public boolean hasSpelExpression() {
return this.hasSpelExpression.get();
return hasValueExpression();
}
/**
* Returns whether the constructor parameter is equipped with a value expression.
*
* @return {@literal true}} if the parameter is equipped with a value expression.
* @since 3.3
*/
public boolean hasValueExpression() {
return this.hasExpression.get();
}
@Override
@@ -157,12 +198,12 @@ public class Parameter<T, P extends PersistentProperty<P>> {
}
return Objects.equals(this.name, that.name) && Objects.equals(this.type, that.type)
&& Objects.equals(this.key, that.key) && Objects.equals(this.entity, that.entity);
&& Objects.equals(this.expression, that.expression) && Objects.equals(this.entity, that.entity);
}
@Override
public int hashCode() {
return Objects.hash(name, type, key, entity);
return Objects.hash(name, type, expression, entity);
}
/**

View File

@@ -35,7 +35,10 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
@@ -44,7 +47,6 @@ import org.springframework.context.EnvironmentAware;
import org.springframework.core.KotlinDetector;
import org.springframework.core.NativeDetector;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.domain.ManagedTypes;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
@@ -52,7 +54,6 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.model.AbstractPersistentProperty;
import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory;
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -63,7 +64,6 @@ import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.data.util.NullableWrapperConverters;
import org.springframework.data.util.Optionals;
@@ -94,7 +94,8 @@ import org.springframework.util.ReflectionUtils.FieldFilter;
* @author Christoph Strobl
*/
public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?, P>, P extends PersistentProperty<P>>
implements MappingContext<E, P>, ApplicationEventPublisherAware, ApplicationContextAware, InitializingBean, EnvironmentAware {
implements MappingContext<E, P>, ApplicationEventPublisherAware, ApplicationContextAware, BeanFactoryAware,
EnvironmentAware, InitializingBean {
private static final Log LOGGER = LogFactory.getLog(MappingContext.class);
@@ -105,7 +106,7 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
private @Nullable ApplicationEventPublisher applicationEventPublisher;
private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT;
private @Nullable EnvironmentAccessor environmentAccessor;
private @Nullable Environment environment;
private ManagedTypes managedTypes = ManagedTypes.empty();
@@ -134,18 +135,47 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Sets the application context. Sets also {@link ApplicationEventPublisher} and {@link Environment} if these weren't
* already set.
*
* @param applicationContext the ApplicationContext object to be used by this object.
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.evaluationContextProvider = new ExtensionAwareEvaluationContextProvider(applicationContext);
setBeanFactory(applicationContext);
if (applicationEventPublisher == null) {
if (this.applicationEventPublisher == null) {
this.applicationEventPublisher = applicationContext;
}
if (this.environment == null) {
this.environment = applicationContext.getEnvironment();
}
}
/**
* @param beanFactory owning BeanFactory.
* @throws BeansException
* @since 3.3
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ListableBeanFactory lbf) {
this.evaluationContextProvider = new ExtensionAwareEvaluationContextProvider(lbf);
}
}
/**
* @param environment the {@code Environment} that this component runs in.
* @since 3.3
*/
@Override
public void setEnvironment(Environment environment) {
this.environmentAccessor = new DelegatingEnvironmentAccessor(environment);
this.environment = environment;
}
/**
@@ -153,7 +183,6 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
*
* @param initialEntitySet
* @see #setManagedTypes(ManagedTypes)
*
*/
public void setInitialEntitySet(Set<? extends Class<?>> initialEntitySet) {
setManagedTypes(ManagedTypes.fromIterable(initialEntitySet));
@@ -210,6 +239,7 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
}
}
@Override
@Nullable
public E getPersistentEntity(Class<?> type) {
return getPersistentEntity(TypeInformation.of(type));
@@ -416,7 +446,9 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
E entity = createPersistentEntity(userTypeInformation);
entity.setEvaluationContextProvider(evaluationContextProvider);
entity.setEnvironmentAccessor(environmentAccessor);
if (environment != null) {
entity.setEnvironment(environment);
}
// Eagerly cache the entity as we might have to find it during recursive lookups.
persistentEntities.put(userTypeInformation, Optional.of(entity));
@@ -488,10 +520,6 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
@Override
public void afterPropertiesSet() {
if(this.environmentAccessor == null) {
this.environmentAccessor = new DelegatingEnvironmentAccessor(new StandardEnvironment());
}
initialize();
}
@@ -594,9 +622,6 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
return;
}
if(property instanceof AbstractPersistentProperty<?> pp) {
pp.setEnvironmentAccessor(environmentAccessor);
}
entity.addPersistentProperty(property);
if (property.isAssociation()) {
@@ -795,31 +820,4 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
}
}
/**
* @author Christoph Strobl
* @since 3.3
*/
public static class DelegatingEnvironmentAccessor implements EnvironmentAccessor {
private final Environment environment;
static EnvironmentAccessor standard() {
return new DelegatingEnvironmentAccessor(new StandardEnvironment());
}
public DelegatingEnvironmentAccessor(Environment environment) {
this.environment = environment;
}
@Nullable
@Override
public String getProperty(String key) {
return environment.getProperty(key);
}
@Override
public String resolvePlaceholders(String text) {
return environment.resolvePlaceholders(text);
}
}
}

View File

@@ -27,7 +27,6 @@ import java.util.stream.Collectors;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.ReflectionUtils;
@@ -75,9 +74,6 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
private final Lazy<Boolean> readable;
private final boolean immutable;
private @Nullable EnvironmentAccessor environmentAccessor;
public AbstractPersistentProperty(Property property, PersistentEntity<?, P> owner,
SimpleTypeHolder simpleTypeHolder) {
@@ -310,14 +306,6 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
return targetType == null ? information.getRequiredActualType() : targetType;
}
protected EnvironmentAccessor getEnvironmentAccessor() {
return environmentAccessor;
}
public void setEnvironmentAccessor(EnvironmentAccessor environmentAccessor) {
this.environmentAccessor = environmentAccessor;
}
@Override
public boolean equals(@Nullable Object obj) {

View File

@@ -37,8 +37,6 @@ import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.ReflectionUtils;
@@ -198,10 +196,12 @@ public abstract class AnnotationBasedPersistentProperty<P extends PersistentProp
return isTransient.get();
}
@Override
public boolean isIdProperty() {
return isId.get();
}
@Override
public boolean isVersionProperty() {
return isVersion.get();
}
@@ -227,6 +227,7 @@ public abstract class AnnotationBasedPersistentProperty<P extends PersistentProp
* @param annotationType must not be {@literal null}.
* @return {@literal null} if annotation type not found on property.
*/
@Override
@Nullable
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
@@ -268,6 +269,7 @@ public abstract class AnnotationBasedPersistentProperty<P extends PersistentProp
* @param annotationType the annotation type to look up.
* @return
*/
@Override
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
return doFindAnnotation(annotationType).isPresent();
}

View File

@@ -30,13 +30,14 @@ import java.util.TreeSet;
import java.util.stream.Collectors;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.env.Environment;
import org.springframework.data.annotation.Immutable;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.domain.Persistable;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.mapping.*;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.data.support.PersistableIsNewStrategy;
import org.springframework.data.util.Lazy;
@@ -80,7 +81,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
private @Nullable P versionProperty;
private PersistentPropertyAccessorFactory propertyAccessorFactory;
private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT;
private @Nullable EnvironmentAccessor environmentAccessor;
private @Nullable Environment environment = null;
private final Lazy<Alias> typeAlias;
private final Lazy<IsNewStrategy> isNewStrategy;
@@ -148,36 +149,44 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
return creator != null && creator.isCreatorParameter(property);
}
@Override
public boolean isIdProperty(PersistentProperty<?> property) {
return idProperty != null && idProperty.equals(property);
}
@Override
public boolean isVersionProperty(PersistentProperty<?> property) {
return versionProperty != null && versionProperty.equals(property);
}
@Override
public String getName() {
return getType().getName();
}
@Override
@Nullable
public P getIdProperty() {
return idProperty;
}
@Override
@Nullable
public P getVersionProperty() {
return versionProperty;
}
@Override
public boolean hasIdProperty() {
return idProperty != null;
}
@Override
public boolean hasVersionProperty() {
return versionProperty != null;
}
@Override
public void addPersistentProperty(P property) {
Assert.notNull(property, "Property must not be null");
@@ -220,9 +229,13 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
this.evaluationContextProvider = provider;
}
/**
* @param environment the {@code Environment} that this component runs in.
* @since 3.3
*/
@Override
public void setEnvironmentAccessor(EnvironmentAccessor accessor) {
this.environmentAccessor = accessor;
public void setEnvironment(Environment environment) {
this.environment = environment;
}
/**
@@ -248,6 +261,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
return property;
}
@Override
public void addAssociation(Association<P> association) {
Assert.notNull(association, "Association must not be null");
@@ -283,18 +297,22 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
.filter(it -> it.isAnnotationPresent(annotationType)).collect(Collectors.toList());
}
@Override
public Class<T> getType() {
return information.getType();
}
@Override
public Alias getTypeAlias() {
return typeAlias.get();
}
@Override
public TypeInformation<T> getTypeInformation() {
return information;
}
@Override
public void doWithProperties(PropertyHandler<P> handler) {
Assert.notNull(handler, "PropertyHandler must not be null");
@@ -314,6 +332,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
}
}
@Override
public void doWithAssociations(AssociationHandler<P> handler) {
Assert.notNull(handler, "Handler must not be null");
@@ -323,6 +342,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
}
}
@Override
public void doWithAssociations(SimpleAssociationHandler handler) {
Assert.notNull(handler, "Handler must not be null");
@@ -350,6 +370,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(getType(), it)));
}
@Override
public void verify() {
if (comparator != null) {
@@ -449,14 +470,26 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
}
/**
* Obtain the {@link EnvironmentAccessor} providing access to the current
* {@link org.springframework.core.env.Environment}.
* Obtain a {@link ValueEvaluationContext} for a {@code rootObject}.
*
* @return never {@literal null}.
* @param rootObject must not be {@literal null}.
* @return the evaluation context including all potential extensions.
* @since 3.3
*/
protected EnvironmentAccessor getEnvironmentAccessor() {
return environmentAccessor;
protected ValueEvaluationContext getValueEvaluationContext(Object rootObject) {
return ValueEvaluationContext.of(this.environment, getEvaluationContext(rootObject));
}
/**
* Obtain a {@link ValueEvaluationContext} for a {@code rootObject} given {@link ExpressionDependencies}.
*
* @param rootObject must not be {@literal null}.
* @param dependencies must not be {@literal null}.
* @return the evaluation context with extensions loaded that satisfy {@link ExpressionDependencies}.
* @since 3.3
*/
protected ValueEvaluationContext getValueEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return ValueEvaluationContext.of(this.environment, getEvaluationContext(rootObject, dependencies));
}
/**
@@ -534,6 +567,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
this.delegate = delegate;
}
@Override
public int compare(@Nullable Association<P> left, @Nullable Association<P> right) {
if (left == null) {

View File

@@ -0,0 +1,88 @@
/*
* 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.mapping.model;
import org.springframework.core.env.EnvironmentCapable;
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.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentLruCache;
/**
* Factory to create a ValueExpressionEvaluator
*
* @author Mark Paluch
* @since 3.3
*/
public class CachingValueExpressionEvaluatorFactory implements ValueEvaluationContextProvider {
private final ConcurrentLruCache<String, ValueExpression> expressionCache;
private final EnvironmentCapable environmentProvider;
private final EvaluationContextProvider evaluationContextProvider;
public CachingValueExpressionEvaluatorFactory(ExpressionParser expressionParser,
EnvironmentCapable environmentProvider, EvaluationContextProvider evaluationContextProvider) {
this(expressionParser, environmentProvider, evaluationContextProvider, 256);
}
public CachingValueExpressionEvaluatorFactory(ExpressionParser expressionParser,
EnvironmentCapable environmentProvider, EvaluationContextProvider evaluationContextProvider, int cacheSize) {
Assert.notNull(expressionParser, "ExpressionParser must not be null");
ValueExpressionParser parser = ValueExpressionParser.create(() -> expressionParser);
this.expressionCache = new ConcurrentLruCache<>(cacheSize, parser::parse);
this.environmentProvider = environmentProvider;
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
public ValueEvaluationContext getEvaluationContext(Object rootObject) {
return ValueEvaluationContext.of(environmentProvider.getEnvironment(),
evaluationContextProvider.getEvaluationContext(rootObject));
}
@Override
public ValueEvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return ValueEvaluationContext.of(environmentProvider.getEnvironment(),
evaluationContextProvider.getEvaluationContext(rootObject, dependencies));
}
/**
* Creates a new {@link ValueExpressionEvaluator} using the given {@code source} as root object.
*
* @param source the root object for evaluating the expression.
* @return a new {@link ValueExpressionEvaluator} to evaluate the expression in the context of the given
* {@code source} object.
*/
public ValueExpressionEvaluator create(Object source) {
return new ValueExpressionEvaluator() {
@SuppressWarnings("unchecked")
@Override
public <T> T evaluate(String expression) {
ValueExpression valueExpression = expressionCache.get(expression);
return (T) valueExpression.evaluate(getEvaluationContext(source, valueExpression.getExpressionDependencies()));
}
};
}
}

View File

@@ -28,7 +28,9 @@ import org.springframework.util.Assert;
* {@link SpelExpressionParser} and {@link EvaluationContext}.
*
* @author Oliver Gierke
* @deprecated since 3.3, use {@link CachingValueExpressionEvaluatorFactory} instead.
*/
@Deprecated(since = "3.3")
public class DefaultSpELExpressionEvaluator implements SpELExpressionEvaluator {
private final Object source;

View File

@@ -15,13 +15,13 @@
*/
package org.springframework.data.mapping.model;
import org.springframework.context.EnvironmentAware;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.support.EnvironmentAccessor;
/**
* Interface capturing mutator methods for {@link PersistentEntity}s.
@@ -29,7 +29,8 @@ import org.springframework.data.support.EnvironmentAccessor;
* @author Oliver Gierke
* @author Mark Paluch
*/
public interface MutablePersistentEntity<T, P extends PersistentProperty<P>> extends PersistentEntity<T, P> {
public interface MutablePersistentEntity<T, P extends PersistentProperty<P>>
extends PersistentEntity<T, P>, EnvironmentAware {
/**
* Adds a {@link PersistentProperty} to the entity.
@@ -68,11 +69,4 @@ public interface MutablePersistentEntity<T, P extends PersistentProperty<P>> ext
*/
void setEvaluationContextProvider(EvaluationContextProvider provider);
/**
* Sets the {@link EnvironmentAccessor} to be used by the entity.
*
* @param accessor must not be {@literal null}.
* @since 3.3
*/
void setEnvironmentAccessor(EnvironmentAccessor accessor);
}

View File

@@ -45,6 +45,7 @@ public class PersistentEntityParameterValueProvider<P extends PersistentProperty
this.parent = parent;
}
@Override
@Nullable
@SuppressWarnings("unchecked")
public <T> T getParameterValue(Parameter<T, P> parameter) {

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mapping.model;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.PropertyAccessor;
@@ -30,7 +31,7 @@ import org.springframework.util.Assert;
*
* @author Oliver Gierke
*/
public class SpELContext {
public class SpELContext implements EvaluationContextProvider {
private final SpelExpressionParser parser;
private final PropertyAccessor accessor;
@@ -90,6 +91,7 @@ public class SpELContext {
return this.parser;
}
@Override
public EvaluationContext getEvaluationContext(Object source) {
StandardEvaluationContext evaluationContext = new StandardEvaluationContext(source);

View File

@@ -15,21 +15,12 @@
*/
package org.springframework.data.mapping.model;
import org.springframework.lang.Nullable;
/**
* SPI for components that can evaluate Spring EL expressions.
*
* @author Oliver Gierke
*/
public interface SpELExpressionEvaluator {
@Deprecated(since = "3.3")
public interface SpELExpressionEvaluator extends ValueExpressionEvaluator {
/**
* Evaluates the given expression.
*
* @param expression
* @return
*/
@Nullable
<T> T evaluate(String expression);
}

View File

@@ -16,9 +16,7 @@
package org.springframework.data.mapping.model;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;
/**
* {@link ParameterValueProvider} that can be used to front a {@link ParameterValueProvider} delegate to prefer a SpEL
@@ -26,43 +24,16 @@ import org.springframework.lang.Nullable;
*
* @author Oliver Gierke
* @author Mark Paluch
* @deprecated since 3.3, use {@link ValueExpressionParameterValueProvider} instead.
*/
@Deprecated(since = "3.3")
public class SpELExpressionParameterValueProvider<P extends PersistentProperty<P>>
implements ParameterValueProvider<P> {
private final SpELExpressionEvaluator evaluator;
private final ConversionService conversionService;
private final ParameterValueProvider<P> delegate;
extends ValueExpressionParameterValueProvider<P> implements ParameterValueProvider<P> {
public SpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, ConversionService conversionService,
ParameterValueProvider<P> delegate) {
this.evaluator = evaluator;
this.conversionService = conversionService;
this.delegate = delegate;
super(evaluator, conversionService, delegate);
}
@Nullable
public <T> T getParameterValue(Parameter<T, P> parameter) {
if (!parameter.hasSpelExpression()) {
return delegate == null ? null : delegate.getParameterValue(parameter);
}
Object object = evaluator.evaluate(parameter.getSpelExpression());
return object == null ? null : potentiallyConvertSpelValue(object, parameter);
}
/**
* Hook to allow to massage the value resulting from the Spel expression evaluation. Default implementation will
* leverage the configured {@link ConversionService} to massage the value into the parameter type.
*
* @param object the value to massage, will never be {@literal null}.
* @param parameter the {@link Parameter} we create the value for
* @return
*/
@Nullable
protected <T> T potentiallyConvertSpelValue(Object object, Parameter<T, P> parameter) {
return conversionService.convert(object, parameter.getRawType());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright 2012-2023 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.
@@ -13,18 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.support;
package org.springframework.data.mapping.model;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 3.3
* SPI for components that can evaluate Value expressions.
*
* @author Mark Paluch
* @since 3.2
*/
public interface EnvironmentAccessor extends PlaceholderResolver {
public interface ValueExpressionEvaluator {
/**
* Evaluates the given expression.
*
* @param expression
* @return
*/
@Nullable
String getProperty(String key);
<T> T evaluate(String expression);
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2012-2023 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.mapping.model;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link ParameterValueProvider} that can be used to front a {@link ParameterValueProvider} delegate to prefer a SpEL
* expression evaluation over directly resolving the parameter value with the delegate.
*
* @author Oliver Gierke
* @author Mark Paluch
* @since 3.3
*/
public class ValueExpressionParameterValueProvider<P extends PersistentProperty<P>>
implements ParameterValueProvider<P> {
private final ValueExpressionEvaluator evaluator;
private final ConversionService conversionService;
private final ParameterValueProvider<P> delegate;
public ValueExpressionParameterValueProvider(ValueExpressionEvaluator evaluator, ConversionService conversionService,
ParameterValueProvider<P> delegate) {
Assert.notNull(evaluator, "ValueExpressionEvaluator must not be null");
Assert.notNull(conversionService, "ConversionService must not be null");
Assert.notNull(delegate, "Delegate must not be null");
this.evaluator = evaluator;
this.conversionService = conversionService;
this.delegate = delegate;
}
@Override
@Nullable
public <T> T getParameterValue(Parameter<T, P> parameter) {
if (!parameter.hasValueExpression()) {
return delegate.getParameterValue(parameter);
}
// retain compatibility where we accepted bare expressions in @Value
String rawExpressionString = parameter.getRequiredValueExpression();
String expressionString = rawExpressionString.contains("#{") || rawExpressionString.contains("${")
? rawExpressionString
: "#{" + rawExpressionString + "}";
Object object = evaluator.evaluate(expressionString);
return object == null ? null : potentiallyConvertExpressionValue(object, parameter);
}
/**
* Hook to allow to massage the value resulting from the Spel expression evaluation. Default implementation will
* leverage the configured {@link ConversionService} to massage the value into the parameter type.
*
* @param object the value to massage, will never be {@literal null}.
* @param parameter the {@link Parameter} we create the value for
* @return the converted parameter value.
* @deprecated since 3.3, use {@link #potentiallyConvertExpressionValue(Object, Parameter)} instead.
*/
@Nullable
@Deprecated(since = "3.3")
protected <T> T potentiallyConvertSpelValue(Object object, Parameter<T, P> parameter) {
return conversionService.convert(object, parameter.getRawType());
}
/**
* Hook to allow to massage the value resulting from the Spel expression evaluation. Default implementation will
* leverage the configured {@link ConversionService} to massage the value into the parameter type.
*
* @param object the value to massage, will never be {@literal null}.
* @param parameter the {@link Parameter} we create the value for
* @return the converted parameter value.
* @since 3.3
*/
@Nullable
protected <T> T potentiallyConvertExpressionValue(Object object, Parameter<T, P> parameter) {
return potentiallyConvertSpelValue(object, parameter);
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.util.AnnotationDetectionMethodCallback;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -46,10 +47,29 @@ import org.springframework.util.ReflectionUtils;
public class SpelAwareProxyProjectionFactory extends ProxyProjectionFactory implements BeanFactoryAware {
private final Map<Class<?>, Boolean> typeCache = new ConcurrentHashMap<>();
private final SpelExpressionParser parser = new SpelExpressionParser();
private final ExpressionParser parser;
private @Nullable BeanFactory beanFactory;
/**
* Create a new {@link SpelAwareProxyProjectionFactory}.
*/
public SpelAwareProxyProjectionFactory() {
this(new SpelExpressionParser());
}
/**
* Create a new {@link SpelAwareProxyProjectionFactory} for a given {@link ExpressionParser}.
*
* @param parser the parser to use.
* @since 3.3
*/
public SpelAwareProxyProjectionFactory(ExpressionParser parser) {
Assert.notNull(parser, "ExpressionParser must not be null");
this.parser = parser;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;

View File

@@ -30,9 +30,9 @@ import org.springframework.context.expression.MapAccessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -71,7 +71,7 @@ class SpelEvaluatingMethodInterceptor implements MethodInterceptor {
* @param targetInterface must not be {@literal null}.
*/
public SpelEvaluatingMethodInterceptor(MethodInterceptor delegate, Object target, @Nullable BeanFactory beanFactory,
SpelExpressionParser parser, Class<?> targetInterface) {
ExpressionParser parser, Class<?> targetInterface) {
Assert.notNull(delegate, "Delegate MethodInterceptor must not be null");
Assert.notNull(target, "Target object must not be null");
@@ -105,7 +105,7 @@ class SpelEvaluatingMethodInterceptor implements MethodInterceptor {
* @return
*/
private static Map<Integer, Expression> potentiallyCreateExpressionsForMethodsOnTargetInterface(
SpelExpressionParser parser, Class<?> targetInterface) {
ExpressionParser parser, Class<?> targetInterface) {
Map<Integer, Expression> expressions = new HashMap<>();

View File

@@ -0,0 +1,5 @@
/**
* SpEL support.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.spel;

View File

@@ -1,128 +0,0 @@
/*
* Copyright 2023. 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
*
* http://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.util;
import java.util.List;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.ConstructorResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.OperatorOverloader;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypeComparator;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.TypeLocator;
import org.springframework.expression.TypedValue;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 2023/11
*/
public abstract class ExpressionEvaluator {
private EnvironmentAccessor environmentAccessor;
private EvaluationContextProvider evaluationContextProvider;
public ExpressionEvaluator(EnvironmentAccessor environmentAccessor, EvaluationContextProvider evaluationContextProvider) {
this.environmentAccessor = environmentAccessor;
this.evaluationContextProvider = evaluationContextProvider;
}
abstract <T> T evaluate(String text, EnvironmentAwareEvaluationContext context);
public class EnvironmentAwareEvaluationContext implements EvaluationContext, EnvironmentAccessor, EvaluationContextProvider {
@Override
public EvaluationContext getEvaluationContext(Object rootObject) {
return null;
}
@Nullable
@Override
public String getProperty(String key) {
return null;
}
@Override
public String resolvePlaceholders(String text) {
return null;
}
@Override
public TypedValue getRootObject() {
return null;
}
@Override
public List<PropertyAccessor> getPropertyAccessors() {
return null;
}
@Override
public List<ConstructorResolver> getConstructorResolvers() {
return null;
}
@Override
public List<MethodResolver> getMethodResolvers() {
return null;
}
@Nullable
@Override
public BeanResolver getBeanResolver() {
return null;
}
@Override
public TypeLocator getTypeLocator() {
return null;
}
@Override
public TypeConverter getTypeConverter() {
return null;
}
@Override
public TypeComparator getTypeComparator() {
return null;
}
@Override
public OperatorOverloader getOperatorOverloader() {
return null;
}
@Override
public void setVariable(String name, @Nullable Object value) {
}
@Nullable
@Override
public Object lookupVariable(String name) {
return null;
}
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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 static org.assertj.core.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* Unit tests for {@link ValueExpression} and its parsing.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
public class ValueEvaluationUnitTests {
private ValueEvaluationContext evaluationContext;
@BeforeEach
void setUp() {
MapPropertySource propertySource = new MapPropertySource("map", Map.of("env.key.one", "value", "env.key.two", 42L));
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().addFirst(propertySource);
this.evaluationContext = new ValueEvaluationContext() {
@Override
public Environment getEnvironment() {
return environment;
}
@Override
public EvaluationContext getEvaluationContext() {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("contextVar", "contextVal");
return context;
}
};
}
@Test // GH-2369
void shouldParseAndEvaluateExpressions() {
assertThat(eval("foo")).isEqualTo("foo");
assertThat(eval("${env.key.one}")).isEqualTo("value");
assertThat(eval("${env.key.two}")).isEqualTo("42");
assertThat(eval("${env.key.one")).isEqualTo("${env.key.one");
assertThat(eval("#{'foo'}-")).isEqualTo("foo-");
assertThat(eval("#{'fo\"o'}-")).isEqualTo("fo\"o-");
assertThat(eval("#{\"foo\"}-")).isEqualTo("foo-");
assertThat(eval("#{\"fo'o\"}-")).isEqualTo("fo'o-");
assertThat(eval("${env.key.one}-")).isEqualTo("value-");
assertThat(eval("#{#contextVar}")).isEqualTo("contextVal");
assertThat(eval("${env.does.not.exist:some-default}-")).isEqualTo("some-default-");
assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> eval("${env.does.not.exist}-"))
.withMessageContaining("Could not resolve placeholder 'env.does.not.exist'");
}
@Test // GH-2369
void shouldParseLiteral() {
ValueParserConfiguration parserContext = () -> new SpelExpressionParser();
ValueExpressionParser parser = ValueExpressionParser.create(parserContext);
assertThat(parser.parse("#{'foo'}-${key.one}").isLiteral()).isFalse();
assertThat(parser.parse("foo").isLiteral()).isTrue();
assertThat(parser.parse("#{'foo'}").isLiteral()).isFalse();
assertThat(parser.parse("${key.one}").isLiteral()).isFalse();
}
@Test // GH-2369
void shouldParseCompoundExpression() {
assertThat(eval("#{'foo'}-")).isEqualTo("foo-");
assertThat(eval("#{'fo\"o'}-")).isEqualTo("fo\"o-");
assertThat(eval("#{\"foo\"}-")).isEqualTo("foo-");
assertThat(eval("#{\"fo'o\"}-")).isEqualTo("fo'o-");
assertThat(eval("${env.key.one}-")).isEqualTo("value-");
assertThat(eval("${env.key.one}-and-some-#{'foo'}-#{#contextVar}")).isEqualTo("value-and-some-foo-contextVal");
}
@Test // GH-2369
void shouldRaiseParseException() {
assertThatExceptionOfType(ParseException.class).isThrownBy(() -> eval("#{'foo'}-${key.one")).withMessageContaining(
"Expression [#{'foo'}-${key.one] @9: No ending suffix '}' for expression starting at character 9: ${key.one");
assertThatExceptionOfType(ParseException.class).isThrownBy(() -> eval("#{'foo'}-${env.key.one"));
assertThatExceptionOfType(ParseException.class).isThrownBy(() -> eval("#{'foo'"));
assertThatExceptionOfType(ParseException.class).isThrownBy(() -> eval("#{#foo - ${numeric.key}}"))
.withMessageContaining("[#foo - ${numeric.key}]");
}
@Test // GH-2369
void shouldParseQuoted() {
assertThat(eval("#{(1+1) + '-foo'+\"-bar\"}")).isEqualTo("2-foo-bar");
assertThat(eval("#{(1+1) + '-foo'+\"-bar}\"}")).isEqualTo("2-foo-bar}");
assertThat(eval("#{(1+1) + '-foo}'}")).isEqualTo("2-foo}");
assertThat(eval("#{(1+1) + '-foo\\}'}")).isEqualTo("2-foo\\}");
assertThat(eval("#{(1+1) + \"-foo'}\" + '-bar}'}")).isEqualTo("2-foo'}-bar}");
}
private String eval(String expressionString) {
ValueParserConfiguration parserContext = SpelExpressionParser::new;
ValueExpressionParser parser = ValueExpressionParser.create(parserContext);
return (String) parser.parse(expressionString).evaluate(evaluationContext);
}
}

View File

@@ -147,7 +147,7 @@ class PreferredConstructorDiscovererUnitTests<P extends PersistentProperty<P>> {
var constructor = PreferredConstructorDiscoverer.discover(ClassWithMetaAnnotatedParameter.class);
assertThat(constructor).isNotNull();
assertThat(constructor.getParameters().get(0).getSpelExpression()).isEqualTo("${hello-world}");
assertThat(constructor.getParameters().get(0).getValueExpression()).isEqualTo("${hello-world}");
assertThat(constructor.getParameters().get(0).getAnnotations()).isNotNull();
}

View File

@@ -0,0 +1,64 @@
/*
* 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.mapping.model;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* Unit tests for {@link CachingValueExpressionEvaluatorFactory}.
*
* @author Mark Paluch
*/
class CachingValueExpressionEvaluatorFactoryUnitTests {
@Test // GH-2369
void shouldEvaluateWithDependencies() {
EvaluationContextProvider contextProviderMock = mock(EvaluationContextProvider.class);
CachingValueExpressionEvaluatorFactory factory = new CachingValueExpressionEvaluatorFactory(
new SpelExpressionParser(), StandardEnvironment::new, contextProviderMock);
ValueExpressionEvaluator evaluator = factory.create(new MyRoot("foo"));
when(contextProviderMock.getEvaluationContext(any(), any(ExpressionDependencies.class)))
.then(invocation -> new StandardEvaluationContext(invocation.getArgument(0)));
Object result = evaluator.evaluate("#{root}");
assertThat(result).isEqualTo("foo");
ArgumentCaptor<ExpressionDependencies> captor = ArgumentCaptor.forClass(ExpressionDependencies.class);
verify(contextProviderMock).getEvaluationContext(any(), captor.capture());
ExpressionDependencies value = captor.getValue();
assertThat(value).hasSize(1).isNotEqualTo(ExpressionDependencies.none());
}
record MyRoot(String root) {
}
}

View File

@@ -53,7 +53,7 @@ class SpelExpressionParameterProviderUnitTests {
provider = new SpELExpressionParameterValueProvider<>(evaluator, conversionService, delegate);
parameter = mock(Parameter.class);
when(parameter.hasSpelExpression()).thenReturn(true);
when(parameter.hasValueExpression()).thenReturn(true);
when(parameter.getRawType()).thenReturn(Object.class);
}
@@ -62,7 +62,7 @@ class SpelExpressionParameterProviderUnitTests {
void delegatesIfParameterDoesNotHaveASpELExpression() {
Parameter<Object, SamplePersistentProperty> parameter = mock(Parameter.class);
when(parameter.hasSpelExpression()).thenReturn(false);
when(parameter.hasValueExpression()).thenReturn(false);
provider.getParameterValue(parameter);
verify(delegate, times(1)).getParameterValue(parameter);
@@ -72,17 +72,17 @@ class SpelExpressionParameterProviderUnitTests {
@Test
void evaluatesSpELExpression() {
when(parameter.getSpelExpression()).thenReturn("expression");
when(parameter.getRequiredValueExpression()).thenReturn("expression");
provider.getParameterValue(parameter);
verify(delegate, times(0)).getParameterValue(parameter);
verify(evaluator, times(1)).evaluate("expression");
verify(evaluator, times(1)).evaluate("#{expression}");
}
@Test
void handsSpELValueToConversionService() {
doReturn("source").when(parameter).getSpelExpression();
doReturn("source").when(parameter).getRequiredValueExpression();
doReturn("value").when(evaluator).evaluate(any());
provider.getParameterValue(parameter);
@@ -94,7 +94,7 @@ class SpelExpressionParameterProviderUnitTests {
@Test
void doesNotConvertNullValue() {
doReturn("source").when(parameter).getSpelExpression();
doReturn("source").when(parameter).getRequiredValueExpression();
doReturn(null).when(evaluator).evaluate(any());
provider.getParameterValue(parameter);
@@ -116,7 +116,7 @@ class SpelExpressionParameterProviderUnitTests {
}
};
doReturn("source").when(parameter).getSpelExpression();
doReturn("source").when(parameter).getRequiredValueExpression();
doReturn("value").when(evaluator).evaluate(any());
assertThat(provider.getParameterValue(parameter)).isEqualTo("FOO");

View File

@@ -0,0 +1,89 @@
/*
* 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.mapping.model;
import static org.assertj.core.api.Assertions.*;
import java.lang.annotation.Annotation;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.expression.MapAccessor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.context.SamplePersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* Unit tests for {@link ValueExpressionParameterValueProvider}.
*
* @author Mark Paluch
*/
class ValueExpressionParameterValueProviderUnitTests {
CachingValueExpressionEvaluatorFactory factory = new CachingValueExpressionEvaluatorFactory(
new SpelExpressionParser(), StandardEnvironment::new, rootObject -> {
SpELContext spELContext = new SpELContext(new MapAccessor());
return spELContext.getEvaluationContext(rootObject);
});
ValueExpressionParameterValueProvider<SamplePersistentProperty> provider = new ValueExpressionParameterValueProvider<>(
factory.create(Map.of("name", "Walter")), DefaultConversionService.getSharedInstance(),
new ParameterValueProvider<SamplePersistentProperty>() {
@Override
public <T> T getParameterValue(Parameter<T, SamplePersistentProperty> parameter) {
return null;
}
});
@Test // GH-2369
void considersValueCompatibilityFormat() {
Annotation[] annotations = CompatibilityConstructor.class.getConstructors()[0].getParameterAnnotations()[0];
String name = provider
.getParameterValue(new Parameter<>("name", TypeInformation.of(String.class), annotations, null));
assertThat(name).isEqualTo("Walter");
}
@Test // GH-2369
void considersValueTemplateFormat() {
Annotation[] annotations = TemplateConstructor.class.getConstructors()[0].getParameterAnnotations()[0];
String name = provider
.getParameterValue(new Parameter<>("name", TypeInformation.of(String.class), annotations, null));
assertThat(name).isEqualTo("Walter");
}
static class CompatibilityConstructor {
public CompatibilityConstructor(@Value("#root.name") String name) {}
}
static class TemplateConstructor {
public TemplateConstructor(@Value("#{#root.name}") String name) {}
}
}

View File

@@ -1,151 +0,0 @@
/*
* Copyright 2023. 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
*
* http://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.
*/
/*
* Copyright 2023. 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
*
* http://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.
*/
/*
* Copyright 2023 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
*
* http://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.mapping.model.justme;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Christoph Strobl
* @since 2023/11
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class PropertySourceUnitTests {
@Value("${my-type.name}") String entityName;
@Autowired ListableBeanFactory beanFactory;
SpelExpressionParser parser = new SpelExpressionParser();
@Configuration
@PropertySource("classpath:persistent-entity.properties")
static class Config {
}
// @Test
void plainContext() {
System.getProperties().forEach((key,value) -> System.out.printf("%s:%s\n", key, value));
Expression expression = parse("#{systemProperties['os.arch']}");
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
Object value = expression.getValue();
System.out.println("va: " + value);
}
Expression parse(String source) {
return parser.parseExpression(source, getParserContext());
}
// @Test
void loadsContext() {
Map<String, PropertyResolver> beansOfType = beanFactory.getBeansOfType(PropertyResolver.class, false, false);
ExtensionAwareEvaluationContextProvider contextProvider = new ExtensionAwareEvaluationContextProvider(beanFactory);
// Expression spelExpression = parser.parseExpression("#{ T(java.lang.Math).random() * 100.0 }",
// getParserContext());
// Expression expression = parser.parseExpression("#{ T(java.lang.Math).random() * 100.0 }");
// Expression expression = parser.parseExpression("#{ systemProperties['user.region'] }", getParserContext());
StandardEvaluationContext evaluationContext = contextProvider.getEvaluationContext(new StandardEnvironment());
SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.OFF, null));
Expression expression = parser.parseExpression("#{systemProperties['os.arch']}", getParserContext());
Object value = expression.getValue(evaluationContext);
System.out.println("value: " + value);
// Expression expression = parser.parseExpression("#{ ${x.y.z} ?: 'defaultValue' }");
// Expression expression = parser.parseExpression("#{'${my-type.name}'}");
// System.out.println("expression: " + expression);
// Object value = expression.getValue(evaluationContext);
// System.out.println("value: " + value);
}
private static ParserContext getParserContext() {
return new ParserContext() {
@Override
public boolean isTemplate() {
return true;
}
@Override
public String getExpressionPrefix() {
return "#{";
}
@Override
public String getExpressionSuffix() {
return "}";
}
};
}
}

View File

@@ -1,259 +0,0 @@
/*
* Copyright 2023. 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
*
* http://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.
*/
/*
* Copyright 2023 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
*
* http://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.util;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.mapping.context.AbstractMappingContext.DelegatingEnvironmentAccessor;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @since 2023/11
*/
public class ExpressionEvaluatorUnitTests {
public static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.OFF, null));
@Test
void intialTests() {
ExpressionEvaluator evaluator = new ExpressionEvaluator(new DelegatingEnvironmentAccessor(new StandardEnvironment()));
{
Object result =
evaluator.prepare("foo-${os.arch}")
.parseWith(SpelExpressionParser::new)
.withContext(StandardEvaluationContext::new)
.evaluate();
System.out.println("result: " + result);
}
{
Object result = evaluator.prepare("#{systemProperties['os.arch']}")
.withContext(() -> new StandardEvaluationContext(new StandardEnvironment()))
.evaluate();
System.out.println("result: " + result);
}
// @Value("#{1+1}-${os.arch}") -> 2-aarch64
// StandardBeanExpressionResolver
{
Expression expression = SPEL_EXPRESSION_PARSER.parseExpression("#{systemProperties['os.arch']}-foo-${os.arch}", ParserContext.TEMPLATE_EXPRESSION);
Object value = expression.getValue(new StandardEvaluationContext(new StandardEnvironment()));
if(value instanceof String s) {
//
}
System.out.println("value: " + value);
}
{
Object result = evaluator.prepare("${os.arch}").evaluate();
System.out.println("result: " + result);
}
{
Object result = evaluator.prepare("foo-${os.arch}").evaluate();
System.out.println("result: " + result);
}
{
// check against - boot
// plcaechoder replacement after spel
Object result = evaluator.prepare("'#{systemProperties['os.arch']}-foo-${os.arch}'")
.withContext(() -> new StandardEvaluationContext(new StandardEnvironment()))
.evaluate();
System.out.println("result: " + result);
}
// evaluator.prepare(source)evaluate(() -> context);
}
static class ExpressionEvaluator {
private final EnvironmentAccessor environmentAccessor;
public ExpressionEvaluator(EnvironmentAccessor environmentAccessor) {
this.environmentAccessor = environmentAccessor;
}
PreparedExpression prepare(String source) {
return this.prepare(() -> source);
}
PreparedExpression prepare(Supplier<String> source) {
return new PreparedExpression(new Supplier<>() {
@Nullable private String cachedValue;
@Override
public String get() {
if (cachedValue != null) {
return cachedValue;
}
String expressionSource = source.get();
String expression = environmentAccessor.resolvePlaceholders(expressionSource);
if (ObjectUtils.nullSafeEquals(expressionSource, expression)) {
cachedValue = expression;
}
System.out.println("using: '" + expression + "' for " + expressionSource);
return expression;
}
});
}
}
static class PreparedExpression {
Supplier<SpelExpressionParser> expressionParser;
Supplier<String> source;
Map<String, Optional<Expression>> expressionCache = new HashMap<>(1, 1F);
public PreparedExpression(Supplier<String> source) {
this.source = source;
expressionParser = Lazy.of(SpelExpressionParser::new);
}
PreparedExpression parseWith(SpelExpressionParser expressionParser) {
return parseWith(() -> expressionParser);
}
PreparedExpression parseWith(Supplier<SpelExpressionParser> expressionParser) {
this.expressionParser = expressionParser;
return this;
}
<T> T evaluate() {
return withContext(SimpleEvaluationContext.forReadOnlyDataBinding().build()).evaluate();
}
ExpressionEvaluation applyReadonlyBinding() {
return withContext(SimpleEvaluationContext.forReadOnlyDataBinding().build());
}
ExpressionEvaluation withContext(EvaluationContext evaluationContext) {
return withContext(() -> evaluationContext);
}
ExpressionEvaluation withContext(Supplier<EvaluationContext> evaluationContext) {
return new ExpressionEvaluation(this, evaluationContext);
}
<T> T doEvaluate(Supplier<EvaluationContext> evaluationContext) {
String expressionString = source.get();
Optional<Expression> expression = expressionCache.computeIfAbsent(expressionString,
(String key) -> Optional.ofNullable(detectExpression(expressionParser.get(), key)));
return (T) expression.map(it -> it.getValue(evaluationContext.get())).orElse(expressionString);
}
@Nullable
private static Expression detectExpression(SpelExpressionParser parser, @Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
Expression expression = parser.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION);
return expression instanceof LiteralExpression ? null : expression;
}
}
static class ExpressionEvaluation {
final PreparedExpression source;
final Supplier<EvaluationContext> evaluationContext;
boolean cacheResult;
Optional<?> cached;
public ExpressionEvaluation(PreparedExpression source, Supplier<EvaluationContext> evaluationContext) {
this.source = source;
this.evaluationContext = evaluationContext;
}
<T> T evaluate() {
if(cacheResult && cached != null) {
return (T) cached.orElse(null);
}
T result = source.doEvaluate(evaluationContext);
if(cacheResult) {
cached = Optional.ofNullable(result);
}
return result;
}
ExpressionEvaluation cache() {
cacheResult = true;
return this;
}
<T> T reevaluate() {
if(cacheResult) {
cached = null;
}
return evaluate();
}
}
}

View File

@@ -1,221 +0,0 @@
/*
* Copyright 2023 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.util;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.mapping.context.AbstractMappingContext.DelegatingEnvironmentAccessor;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @since 2023/11
*/
public class ExpressionResolverUnitTests {
@Test
void xxx() {
ParserContext parserContext = new DefaultParserContext(new DelegatingEnvironmentAccessor(new StandardEnvironment()), new SpelExpressionParser());
ValueParser parser = ValueParser.create(parserContext);
ParsableStatement statement = parser.prepareStatement("foo-${os.arch}");
// PreparedStatement bindNow = parser.parse("foo-${os.arch}");
statement.evaluate(ValueContext.spelContext(new StandardEvaluationContext()));
}
interface ValueParser {
ParsableStatement prepareStatement(String expression);
static ValueParser create(ParserContext parserContext) {
return new DefaultValueParser(parserContext);
}
}
static class DefaultValueParser implements ValueParser {
private final ParserContext parserContext;
public DefaultValueParser(ParserContext parserContext) {
this.parserContext = parserContext;
}
@Override
public ParsableStatement prepareStatement(String expression) {
return new RawStatement(expression, parserContext);
}
}
interface ParsableStatement {
<T> T evaluate(ValueContext context);
default ParsableStatement transform(Function<ParsableStatement, ParsableStatement> mappingFunction) {
return mappingFunction.apply(this);
}
String toString();
}
class LazyEvaluatingStatement {
}
abstract static class AbstractStatement implements ParsableStatement {
final ParserContext parserContext;
public AbstractStatement(ParserContext parserContext) {
this.parserContext = parserContext;
}
@Override
public <T> T evaluate(ValueContext context) {
ParsableStatement executableStatement = transform(parserContext::resolvePlaceholders)
.transform(parserContext::parseExpressions);
return executableStatement.evaluate(context);
}
abstract String getValue();
}
static class RawStatement extends AbstractStatement {
final String value;
public RawStatement(String value, ParserContext context) {
super(context);
this.value = value;
}
@Override
String getValue() {
return value;
}
}
static class PreparedStatement extends RawStatement {
public PreparedStatement(String value, ParserContext context) {
super(value, context);
}
}
static class ExpressionStatement implements ParsableStatement {
Expression expression;
public ExpressionStatement(Expression expression) {
this.expression = expression;
}
@Override
public <T> T evaluate(ValueContext context) {
if(context instanceof SpELValueContext spelContext) {
return (T) expression.getValue(spelContext.getEvaluationContext());
}
return (T) expression.getValue();
}
@Override
public String toString() {
return expression.getExpressionString();
}
}
interface ParserContext {
ParsableStatement resolvePlaceholders(ParsableStatement statement);
ParsableStatement parseExpressions(ParsableStatement statement);
}
static class DefaultParserContext implements ParserContext {
private final EnvironmentAccessor environmentAccessor;
private final SpelExpressionParser spelExpressionParser;
public DefaultParserContext(EnvironmentAccessor environmentAccessor, SpelExpressionParser spelExpressionParser) {
this.environmentAccessor = environmentAccessor;
this.spelExpressionParser = spelExpressionParser;
}
@Override
public ParsableStatement resolvePlaceholders(ParsableStatement statement) {
return statement.transform(it -> {
if (it instanceof PreparedStatement) {
return it;
}
return new PreparedStatement(environmentAccessor.resolvePlaceholders(it.toString()), this);
});
}
@Override
public ParsableStatement parseExpressions(ParsableStatement statement) {
return statement.transform(it -> {
Expression expression = detectExpression(spelExpressionParser, it.toString());
if (expression == null || expression instanceof LiteralExpression) {
return it;
}
return new ExpressionStatement(expression);
});
}
@Nullable
private static Expression detectExpression(SpelExpressionParser parser, @Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
Expression expression = parser.parseExpression(potentialExpression, org.springframework.expression.ParserContext.TEMPLATE_EXPRESSION);
return expression instanceof LiteralExpression ? null : expression;
}
}
sealed interface ValueContext permits SpELValueContext {
static ValueContext spelContext(EvaluationContext evaluationContext) {
return new SpELValueContext(evaluationContext);
}
}
static final class SpELValueContext implements ValueContext {
EvaluationContext evaluationContext;
public SpELValueContext(EvaluationContext evaluationContext) {
this.evaluationContext = evaluationContext;
}
EvaluationContext getEvaluationContext() {
return evaluationContext;
}
}
}