Skip to content

Commit 67163d0

Browse files
committed
Evolve ValueExpressionParser.
Introduce literal, expression and placeholder variants. Add parser for composite expressions. Closes #2369 Original pull request: #3036
1 parent 9f13e54 commit 67163d0

40 files changed

+1650
-911
lines changed

Diff for: src/main/antora/modules/ROOT/nav.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
** xref:repositories/null-handling.adoc[]
1616
** xref:repositories/projections.adoc[]
1717
* xref:query-by-example.adoc[]
18+
* xref:value-expressions.adoc[]
1819
* xref:auditing.adoc[]
1920
* xref:custom-conversions.adoc[]
2021
* xref:entity-callbacks.adoc[]
+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
[[valueexpressions.fundamentals]]
2+
= Value Expressions Fundamentals
3+
4+
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].
5+
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.
6+
7+
Expressions are expected to be defined by a trusted input such as an annotation value and not to be determined from user input.
8+
9+
The following code demonstrates how to use expressions in the context of annotations.
10+
11+
.Annotation Usage
12+
====
13+
[source,java]
14+
----
15+
@Document("orders-#{tenantService.getOrderCollection()}-${tenant-config.suffix}")
16+
class Order {
17+
// …
18+
}
19+
----
20+
====
21+
22+
Value Expressions can be defined from a sole SpEL Expression, a Property Placeholder or a composite expression mixing various expressions including literals.
23+
24+
.Expression Examples
25+
====
26+
[source]
27+
----
28+
#{tenantService.getOrderCollection()} <1>
29+
#{(1+1) + '-hello-world'} <2>
30+
${tenant-config.suffix} <3>
31+
orders-${tenant-config.suffix} <4>
32+
#{tenantService.getOrderCollection()}-${tenant-config.suffix} <5>
33+
----
34+
35+
<1> Value Expression using a single SpEL Expression.
36+
<2> Value Expression using a static SpEL Expression evaluating to `2-hello-world`.
37+
<3> Value Expression using a single Property Placeholder.
38+
<4> Composite expression comprised of the literal `orders-` and the Property Placeholder `${tenant-config.suffix}`.
39+
<5> Composite expression using SpEL, Property Placeholders and literals.
40+
====
41+
42+
NOTE: Using value expressions introduces a lot of flexibility to your code.
43+
Doing so requires evaluation of the expression on each usage and, therefore, value expression evaluation has an impact on the performance profile.
44+
45+
[[valueexpressions.api]]
46+
== Parsing and Evaluation
47+
48+
Value Expressions are parsed by the `ValueExpressionParser` API.
49+
Instances of `ValueExpression` are thread-safe and can be cached for later use to avoid repeated parsing.
50+
51+
The following example shows the Value Expression API usage:
52+
53+
.Parsing and Evaluation
54+
[tabs]
55+
======
56+
Java::
57+
+
58+
[source,java,role="primary"]
59+
----
60+
ValueParserConfiguration configuration = SpelExpressionParser::new;
61+
ValueEvaluationContext context = ValueEvaluationContext.of(environment, evaluationContext);
62+
63+
ValueExpressionParser parser = ValueExpressionParser.create(configuration);
64+
ValueExpression expression = parser.parse("Hello, World");
65+
Object result = expression.evaluate(context);
66+
----
67+
68+
Kotlin::
69+
+
70+
[source,kotlin,role="secondary"]
71+
----
72+
val configuration = ValueParserConfiguration { SpelExpressionParser() }
73+
val context = ValueEvaluationContext.of(environment, evaluationContext)
74+
75+
val parser = ValueExpressionParser.create(configuration)
76+
val expression: ValueExpression = parser.parse("Hello, World")
77+
val result: Any = expression.evaluate(context)
78+
----
79+
======
80+
81+
[[valueexpressions.spel]]
82+
== SpEL Expressions
83+
84+
{spring-framework-docs}/core/expressions.html[SpEL Expressions] follow the Template style where the expression is expected to be enclosed within the `#{…}` format.
85+
Expressions are evaluated using an `EvaluationContext` that is provided by `EvaluationContextProvider`.
86+
The context itself is a powerful `StandardEvaluationContext` allowing a wide range of operations, access to static types and context extensions.
87+
88+
NOTE: Make sure to parse and evaluate only expressions from trusted sources such as annotations.
89+
Accepting user-provided expressions can create an entry path to exploit the application context and your system resulting in a potential security vulnerability.
90+
91+
=== Extending the Evaluation Context
92+
93+
`EvaluationContextProvider` and its reactive variant `ReactiveEvaluationContextProvider` provide access to an `EvaluationContext`.
94+
`ExtensionAwareEvaluationContextProvider` and its reactive variant `ReactiveExtensionAwareEvaluationContextProvider` are default implementations that determine context extensions from an application context, specifically `ListableBeanFactory`.
95+
96+
Extensions implement either `EvaluationContextExtension` or `ReactiveEvaluationContextExtension` to provide extension support to hydrate `EvaluationContext`.
97+
That are a root object, properties and functions (top-level methods).
98+
99+
The following example shows a context extension that provides a root object, properties, functions and an aliased function.
100+
101+
.Implementing a `EvaluationContextExtension`
102+
[tabs]
103+
======
104+
Java::
105+
+
106+
[source,java,role="primary"]
107+
----
108+
@Component
109+
public class MyExtension implements EvaluationContextExtension {
110+
111+
@Override
112+
public String getExtensionId() {
113+
return "my-extension";
114+
}
115+
116+
@Override
117+
public Object getRootObject() {
118+
return new CustomExtensionRootObject();
119+
}
120+
121+
@Override
122+
public Map<String, Object> getProperties() {
123+
124+
Map<String, Object> properties = new HashMap<>();
125+
126+
properties.put("key", "Hello");
127+
128+
return properties;
129+
}
130+
131+
@Override
132+
public Map<String, Function> getFunctions() {
133+
134+
Map<String, Function> functions = new HashMap<>();
135+
136+
try {
137+
functions.put("aliasedMethod", new Function(getClass().getMethod("extensionMethod")));
138+
return functions;
139+
} catch (Exception o_O) {
140+
throw new RuntimeException(o_O);
141+
}
142+
}
143+
144+
public static String extensionMethod() {
145+
return "Hello World";
146+
}
147+
148+
public static int add(int i1, int i2) {
149+
return i1 + i2;
150+
}
151+
152+
}
153+
154+
public class CustomExtensionRootObject {
155+
156+
public boolean rootObjectInstanceMethod() {
157+
return true;
158+
}
159+
160+
}
161+
----
162+
163+
Kotlin::
164+
+
165+
[source,kotlin,role="secondary"]
166+
----
167+
@Component
168+
class MyExtension : EvaluationContextExtension {
169+
170+
override fun getExtensionId(): String {
171+
return "my-extension"
172+
}
173+
174+
override fun getRootObject(): Any? {
175+
return CustomExtensionRootObject()
176+
}
177+
178+
override fun getProperties(): Map<String, Any> {
179+
val properties: MutableMap<String, Any> = HashMap()
180+
181+
properties["key"] = "Hello"
182+
183+
return properties
184+
}
185+
186+
override fun getFunctions(): Map<String, Function> {
187+
val functions: MutableMap<String, Function> = HashMap()
188+
189+
try {
190+
functions["aliasedMethod"] = Function(javaClass.getMethod("extensionMethod"))
191+
return functions
192+
} catch (o_O: Exception) {
193+
throw RuntimeException(o_O)
194+
}
195+
}
196+
197+
companion object {
198+
fun extensionMethod(): String {
199+
return "Hello World"
200+
}
201+
202+
fun add(i1: Int, i2: Int): Int {
203+
return i1 + i2
204+
}
205+
}
206+
}
207+
208+
class CustomExtensionRootObject {
209+
fun rootObjectInstanceMethod(): Boolean {
210+
return true
211+
}
212+
}
213+
----
214+
======
215+
216+
Once the above shown extension is registered, you can use its exported methods, properties and root object to evaluate SpEL expressions:
217+
218+
.Expression Evaluation Examples
219+
====
220+
[source]
221+
----
222+
#{add(1, 2)} <1>
223+
#{extensionMethod()} <2>
224+
#{aliasedMethod()} <3>
225+
#{key} <4>
226+
#{rootObjectInstanceMethod()} <5>
227+
----
228+
229+
<1> Invoke the method `add` declared by `MyExtension` resulting in `3` as the method adds both numeric parameters and returns the sum.
230+
<2> Invoke the method `extensionMethod` declared by `MyExtension` resulting in `Hello World`.
231+
<3> Invoke the method `aliasedMethod`.
232+
The method is exposed as function and redirects into the method `extensionMethod` declared by `MyExtension` resulting in `Hello World`.
233+
<4> Evaluate the `key` property resulting in `Hello`.
234+
<5> Invoke the method `rootObjectInstanceMethod` on the root object instance `CustomExtensionRootObject`.
235+
====
236+
237+
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`].
238+
239+
[[valueexpressions.property-placeholders]]
240+
== Property Placeholders
241+
242+
Property placeholders following the form `${…}` refer to properties provided typically by a `PropertySource` through `Environment`.
243+
Properties are useful to resolve against system properties, application configuration files, environment configuration or property sources contributed by secret management systems.
244+
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].
245+
246+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.expression;
17+
18+
import java.util.List;
19+
20+
import org.springframework.data.spel.ExpressionDependencies;
21+
22+
/**
23+
* Composite {@link ValueExpression} consisting of multiple placeholder, SpEL, and literal expressions.
24+
*
25+
* @param raw
26+
* @param expressions
27+
* @author Mark Paluch
28+
* @since 3.3
29+
*/
30+
record CompositeValueExpression(String raw, List<ValueExpression> expressions) implements ValueExpression {
31+
32+
@Override
33+
public String getExpressionString() {
34+
return raw;
35+
}
36+
37+
@Override
38+
public ExpressionDependencies getExpressionDependencies() {
39+
40+
ExpressionDependencies dependencies = ExpressionDependencies.none();
41+
42+
for (ValueExpression expression : expressions) {
43+
ExpressionDependencies dependency = expression.getExpressionDependencies();
44+
if (!dependency.equals(ExpressionDependencies.none())) {
45+
dependencies = dependencies.mergeWith(dependency);
46+
}
47+
}
48+
49+
return dependencies;
50+
}
51+
52+
@Override
53+
public boolean isLiteral() {
54+
55+
for (ValueExpression expression : expressions) {
56+
if (!expression.isLiteral()) {
57+
return false;
58+
}
59+
}
60+
61+
return true;
62+
}
63+
64+
@Override
65+
public String evaluate(ValueEvaluationContext context) {
66+
67+
StringBuilder builder = new StringBuilder();
68+
69+
for (ValueExpression expression : expressions) {
70+
builder.append((String) expression.evaluate(context));
71+
}
72+
73+
return builder.toString();
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.expression;
17+
18+
import org.springframework.core.env.Environment;
19+
import org.springframework.expression.EvaluationContext;
20+
21+
/**
22+
* Default {@link ValueEvaluationContext}.
23+
*
24+
* @author Mark Paluch
25+
* @since 3.3
26+
*/
27+
record DefaultValueEvaluationContext(Environment environment,
28+
EvaluationContext evaluationContext) implements ValueEvaluationContext {
29+
30+
@Override
31+
public Environment getEnvironment() {
32+
return environment();
33+
}
34+
35+
@Override
36+
public EvaluationContext getEvaluationContext() {
37+
return evaluationContext();
38+
}
39+
}

0 commit comments

Comments
 (0)