Skip to content

Add ValueExpression infrastructure #3036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.x-2369-SNAPSHOT</version>

<name>Spring Data Core</name>
<description>Core Spring concepts underpinning every Spring Data module.</description>
Expand Down
1 change: 1 addition & 0 deletions src/main/antora/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
246 changes: 246 additions & 0 deletions src/main/antora/modules/ROOT/pages/value-expressions.adoc
Original file line number Diff line number Diff line change
@@ -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].


Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading