diff --git a/pom.xml b/pom.xml
index 8576363d34..ce9bcb2231 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.dataspring-data-commons
- 3.3.0-SNAPSHOT
+ 3.3.x-2369-SNAPSHOTSpring Data CoreCore Spring concepts underpinning every Spring Data module.
diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc
index d5cd181f8e..2f483a6549 100644
--- a/src/main/antora/modules/ROOT/nav.adoc
+++ b/src/main/antora/modules/ROOT/nav.adoc
@@ -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[]
diff --git a/src/main/antora/modules/ROOT/pages/value-expressions.adoc b/src/main/antora/modules/ROOT/pages/value-expressions.adoc
new file mode 100644
index 0000000000..752d3d69ec
--- /dev/null
+++ b/src/main/antora/modules/ROOT/pages/value-expressions.adoc
@@ -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 getProperties() {
+
+ Map properties = new HashMap<>();
+
+ properties.put("key", "Hello");
+
+ return properties;
+ }
+
+ @Override
+ public Map getFunctions() {
+
+ Map 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 {
+ val properties: MutableMap = HashMap()
+
+ properties["key"] = "Hello"
+
+ return properties
+ }
+
+ override fun getFunctions(): Map {
+ val functions: MutableMap = 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].
+
+
diff --git a/src/main/java/org/springframework/data/expression/CompositeValueExpression.java b/src/main/java/org/springframework/data/expression/CompositeValueExpression.java
new file mode 100644
index 0000000000..d2dc64a48a
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/CompositeValueExpression.java
@@ -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 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();
+ }
+}
diff --git a/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java b/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java
new file mode 100644
index 0000000000..d498560135
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java
@@ -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();
+ }
+}
diff --git a/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java b/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java
new file mode 100644
index 0000000000..ceea642641
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java
@@ -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 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;
+ }
+}
diff --git a/src/main/java/org/springframework/data/expression/ExpressionExpression.java b/src/main/java/org/springframework/data/expression/ExpressionExpression.java
new file mode 100644
index 0000000000..83da26614e
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/ExpressionExpression.java
@@ -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();
+ }
+}
diff --git a/src/main/java/org/springframework/data/expression/LiteralValueExpression.java b/src/main/java/org/springframework/data/expression/LiteralValueExpression.java
new file mode 100644
index 0000000000..764220bfc6
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/LiteralValueExpression.java
@@ -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;
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/expression/PlaceholderExpression.java b/src/main/java/org/springframework/data/expression/PlaceholderExpression.java
new file mode 100644
index 0000000000..1473f1ed28
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/PlaceholderExpression.java
@@ -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;
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java b/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java
new file mode 100644
index 0000000000..5be091d8ca
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java
@@ -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();
+}
diff --git a/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java b/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java
new file mode 100644
index 0000000000..05d7d4de2f
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java
@@ -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);
+ }
+}
diff --git a/src/main/java/org/springframework/data/expression/ValueExpression.java b/src/main/java/org/springframework/data/expression/ValueExpression.java
new file mode 100644
index 0000000000..8f4545bcd8
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/ValueExpression.java
@@ -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;
+
+}
diff --git a/src/main/java/org/springframework/data/expression/ValueExpressionParser.java b/src/main/java/org/springframework/data/expression/ValueExpressionParser.java
new file mode 100644
index 0000000000..89f9f65cdc
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/ValueExpressionParser.java
@@ -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.
+ *
+ *
+ * @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;
+
+}
diff --git a/src/main/java/org/springframework/data/expression/ValueParserConfiguration.java b/src/main/java/org/springframework/data/expression/ValueParserConfiguration.java
new file mode 100644
index 0000000000..424ece5856
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/ValueParserConfiguration.java
@@ -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();
+
+}
diff --git a/src/main/java/org/springframework/data/expression/package-info.java b/src/main/java/org/springframework/data/expression/package-info.java
new file mode 100644
index 0000000000..35ebb9ed7b
--- /dev/null
+++ b/src/main/java/org/springframework/data/expression/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Value Expression implementation.
+ */
+@org.springframework.lang.NonNullApi
+package org.springframework.data.expression;
diff --git a/src/main/java/org/springframework/data/mapping/Parameter.java b/src/main/java/org/springframework/data/mapping/Parameter.java
index 9738f93fc8..1254c2a37d 100644
--- a/src/main/java/org/springframework/data/mapping/Parameter.java
+++ b/src/main/java/org/springframework/data/mapping/Parameter.java
@@ -39,11 +39,11 @@ public class Parameter> {
private final @Nullable String name;
private final TypeInformation type;
private final MergedAnnotations annotations;
- private final String key;
+ private final @Nullable String expression;
private final @Nullable PersistentEntity entity;
private final Lazy enclosingClassCache;
- private final Lazy hasSpelExpression;
+ private final Lazy hasExpression;
/**
* Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of
@@ -64,7 +64,7 @@ public Parameter(@Nullable String name, TypeInformation type, Annotation[] an
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 Parameter(@Nullable String name, TypeInformation type, Annotation[] an
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 getRawType() {
}
/**
- * 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 boolean equals(@Nullable Object o) {
}
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);
}
/**
diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
index 4c2ecc88f2..145ce20c80 100644
--- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
+++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
@@ -35,13 +35,18 @@
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;
import org.springframework.context.ApplicationEventPublisherAware;
+import org.springframework.context.EnvironmentAware;
import org.springframework.core.KotlinDetector;
import org.springframework.core.NativeDetector;
+import org.springframework.core.env.Environment;
import org.springframework.data.domain.ManagedTypes;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
@@ -89,7 +94,8 @@
* @author Christoph Strobl
*/
public abstract class AbstractMappingContext, P extends PersistentProperty