mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-25 16:29:28 +00:00
Support Optional with null-safe and Elvis operators in SpEL expressions
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:
- PropertyOrFieldReference
- MethodReference
- Indexer
- Projection
- Selection
- Elvis
Specifically, when a null-safe operator is applied to an empty
`Optional`, it will be treated as if the `Optional` were `null`, and
the subsequent operation will evaluate to `null`. However, if a
null-safe operator is applied to a non-empty `Optional`, the subsequent
operation will be applied to the object contained in the `Optional`,
thereby effectively unwrapping the `Optional`.
For example, if `user` is of type `Optional<User>`, the expression
`user?.name` will evaluate to `null` if `user` is either `null` or an
empty `Optional` and will otherwise evaluate to the `name` of the
`user`, effectively `user.get().getName()` for property access.
Note, however, that invocations of methods defined in the `Optional`
API are still supported on an empty `Optional`. For example, if `name`
is of type `Optional<String>`, the expression `name?.orElse('Unknown')`
will evaluate to "Unknown" if `name` is an empty `Optional` and will
otherwise evaluate to the `String` contained in the `Optional` if
`name` is a non-empty `Optional`, effectively `name.get()`.
Closes gh-20433
This commit is contained in:
+138
@@ -215,6 +215,144 @@ class OptionalNullSafetyTests {
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class NullSafeTests {
|
||||
|
||||
@Test
|
||||
void accessPropertyOnEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findJediByName('')?.name");
|
||||
|
||||
// Invoke multiple times to ensure there are no caching issues.
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessPropertyOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findJediByName('Yoda')?.name");
|
||||
|
||||
// Invoke multiple times to ensure there are no caching issues.
|
||||
assertThat(expr.getValue(context)).isEqualTo("Yoda");
|
||||
assertThat(expr.getValue(context)).isEqualTo("Yoda");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeMethodOnEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findJediByName('')?.salutation('Master')");
|
||||
|
||||
// Invoke multiple times to ensure there are no caching issues.
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeMethodOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findJediByName('Yoda')?.salutation('Master')");
|
||||
|
||||
// Invoke multiple times to ensure there are no caching issues.
|
||||
assertThat(expr.getValue(context)).isEqualTo("Master Yoda");
|
||||
assertThat(expr.getValue(context)).isEqualTo("Master Yoda");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessIndexOnEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.[1]");
|
||||
|
||||
// Invoke multiple times to ensure there are no caching issues.
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessIndexOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.[1]");
|
||||
|
||||
// Invoke multiple times to ensure there are no caching issues.
|
||||
assertThat(expr.getValue(context)).isEqualTo("lemon");
|
||||
assertThat(expr.getValue(context)).isEqualTo("lemon");
|
||||
}
|
||||
|
||||
@Test
|
||||
void projectionOnEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.![#this.length]");
|
||||
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void projectionOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.![#this.length]");
|
||||
|
||||
assertThat(expr.getValue(context, List.class)).containsExactly(6, 5, 5, 9);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAllOnEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.?[#this.length > 5]");
|
||||
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void selectAllOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.?[#this.length > 5]");
|
||||
|
||||
assertThat(expr.getValue(context, List.class)).containsExactly("banana", "pineapple");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectFirstOnEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.^[#this.length > 5]");
|
||||
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void selectFirstOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.^[#this.length > 5]");
|
||||
|
||||
assertThat(expr.getValue(context, List.class)).containsExactly("banana");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectLastOnEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.$[#this.length > 5]");
|
||||
|
||||
assertThat(expr.getValue(context)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void selectLastOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.$[#this.length > 5]");
|
||||
|
||||
assertThat(expr.getValue(context, List.class)).containsExactly("pineapple");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ElvisTests {
|
||||
|
||||
@Test
|
||||
void elvisOperatorOnEmptyOptional() {
|
||||
Expression expr = parser.parseExpression("#service.findJediByName('') ?: 'unknown'");
|
||||
|
||||
assertThat(expr.getValue(context)).isEqualTo("unknown");
|
||||
}
|
||||
|
||||
@Test
|
||||
void elvisOperatorOnNonEmptyOptional() {
|
||||
Expression expr = parser.parseExpression("#service.findJediByName('Yoda') ?: 'unknown'");
|
||||
|
||||
assertThat(expr.getValue(context)).isEqualTo(new Jedi("Yoda"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
record Jedi(String name) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user