Browse Source

Document null-safe collection selection/projection support in SpEL

Closes gh-32208
pull/32243/head
Sam Brannen 2 years ago
parent
commit
4a5dc7c1b0
  1. 7
      framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc
  2. 7
      framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc
  3. 224
      framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc
  4. 79
      spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java

7
framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc

@ -34,5 +34,12 @@ evaluated against each entry in the map (represented as a Java `Map.Entry`). The @@ -34,5 +34,12 @@ evaluated against each entry in the map (represented as a Java `Map.Entry`). The
of a projection across a map is a list that consists of the evaluation of the projection
expression against each map entry.
[NOTE]
====
The Spring Expression Language also supports safe navigation for collection projection.
See
xref:core/expressions/language-ref/operator-safe-navigation.adoc#expressions-operator-safe-navigation-selection-and-projection[Safe Collection Selection and Projection]
for details.
====

7
framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc

@ -59,5 +59,12 @@ the last element. To obtain the first element matching the selection expression, @@ -59,5 +59,12 @@ the last element. To obtain the first element matching the selection expression,
syntax is `.^[selectionExpression]`. To obtain the last element matching the selection
expression, the syntax is `.$[selectionExpression]`.
[NOTE]
====
The Spring Expression Language also supports safe navigation for collection selection.
See
xref:core/expressions/language-ref/operator-safe-navigation.adoc#expressions-operator-safe-navigation-selection-and-projection[Safe Collection Selection and Projection]
for details.
====

224
framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc

@ -7,6 +7,9 @@ language. Typically, when you have a reference to an object, you might need to v @@ -7,6 +7,9 @@ language. Typically, when you have a reference to an object, you might need to v
that it is not `null` before accessing methods or properties of the object. To avoid
this, the safe navigation operator returns `null` instead of throwing an exception.
[[expressions-operator-safe-navigation-property-access]]
== Safe Property and Method Access
The following example shows how to use the safe navigation operator for property access
(`?.`).
@ -59,3 +62,224 @@ Kotlin:: @@ -59,3 +62,224 @@ Kotlin::
<2> Use safe navigation operator on null `placeOfBirth` property
======
[NOTE]
====
The safe navigation operator also applies to method invocations on an object.
For example, the expression `#calculator?.max(4, 2)` evaluates to `null` if the
`#calculator` variable has not been configured in the context. Otherwise, the
`max(int, int)` method will be invoked on the `#calculator`.
====
[[expressions-operator-safe-navigation-selection-and-projection]]
== Safe Collection Selection and Projection
The Spring Expression Language supports safe navigation for
xref:core/expressions/language-ref/collection-selection.adoc[collection selection] and
xref:core/expressions/language-ref/collection-projection.adoc[collection projection] via
the following operators.
* null-safe selection: `?.?`
* null-safe select first: `?.^`
* null-safe select last: `?.$`
* null-safe projection: `?.!`
The following example shows how to use the safe navigation operator for collection
selection (`?.?`).
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
ExpressionParser parser = new SpelExpressionParser();
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression = "members?.?[nationality == 'Serbian']"; // <1>
// evaluates to [Inventor("Nikola Tesla")]
List<Inventor> list = (List<Inventor>) parser.parseExpression(expression)
.getValue(context);
society.members = null;
// evaluates to null - does not throw a NullPointerException
list = (List<Inventor>) parser.parseExpression(expression)
.getValue(context);
----
<1> Use null-safe selection operator on potentially null `members` list
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)
val expression = "members?.?[nationality == 'Serbian']" // <1>
// evaluates to [Inventor("Nikola Tesla")]
var list = parser.parseExpression(expression)
.getValue(context) as List<Inventor>
society.members = null
// evaluates to null - does not throw a NullPointerException
list = parser.parseExpression(expression)
.getValue(context) as List<Inventor>
----
<1> Use null-safe selection operator on potentially null `members` list
======
The following example shows how to use the "null-safe select first" operator for
collections (`?.^`).
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
ExpressionParser parser = new SpelExpressionParser();
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression =
"members?.^[nationality == 'Serbian' || nationality == 'Idvor']"; // <1>
// evaluates to Inventor("Nikola Tesla")
Inventor inventor = parser.parseExpression(expression)
.getValue(context, Inventor.class);
society.members = null;
// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
.getValue(context, Inventor.class);
----
<1> Use "null-safe select first" operator on potentially null `members` list
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)
val expression =
"members?.^[nationality == 'Serbian' || nationality == 'Idvor']" // <1>
// evaluates to Inventor("Nikola Tesla")
var inventor = parser.parseExpression(expression)
.getValue(context, Inventor::class.java)
society.members = null
// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
.getValue(context, Inventor::class.java)
----
<1> Use "null-safe select first" operator on potentially null `members` list
======
The following example shows how to use the "null-safe select last" operator for
collections (`?.$`).
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
ExpressionParser parser = new SpelExpressionParser();
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression =
"members?.$[nationality == 'Serbian' || nationality == 'Idvor']"; // <1>
// evaluates to Inventor("Pupin")
Inventor inventor = parser.parseExpression(expression)
.getValue(context, Inventor.class);
society.members = null;
// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
.getValue(context, Inventor.class);
----
<1> Use "null-safe select last" operator on potentially null `members` list
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)
val expression =
"members?.$[nationality == 'Serbian' || nationality == 'Idvor']" // <1>
// evaluates to Inventor("Pupin")
var inventor = parser.parseExpression(expression)
.getValue(context, Inventor::class.java)
society.members = null
// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
.getValue(context, Inventor::class.java)
----
<1> Use "null-safe select last" operator on potentially null `members` list
======
The following example shows how to use the safe navigation operator for collection
projection (`?.!`).
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
ExpressionParser parser = new SpelExpressionParser();
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
// evaluates to ["Smiljan", "Idvor"]
List placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1>
.getValue(context, List.class);
society.members = null;
// evaluates to null - does not throw a NullPointerException
placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2>
.getValue(context, List.class);
----
<1> Use null-safe projection operator on non-null `members` list
<2> Use null-safe projection operator on null `members` list
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)
// evaluates to ["Smiljan", "Idvor"]
var placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1>
.getValue(context, List::class.java)
society.members = null
// evaluates to null - does not throw a NullPointerException
placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2>
.getValue(context, List::class.java)
----
<1> Use null-safe projection operator on non-null `members` list
<2> Use null-safe projection operator on null `members` list
======

79
spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java

@ -648,6 +648,85 @@ class SpelDocumentationTests extends AbstractExpressionTests { @@ -648,6 +648,85 @@ class SpelDocumentationTests extends AbstractExpressionTests {
.getValue(context, tesla, String.class);
assertThat(city).isNull();
}
@Test
@SuppressWarnings("unchecked")
void nullSafeSelection() {
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression = "members?.?[nationality == 'Serbian']"; // <1>
// evaluates to [Inventor("Nikola Tesla")]
List<Inventor> list = (List<Inventor>) parser.parseExpression(expression)
.getValue(context);
assertThat(list).map(Inventor::getName).containsOnly("Nikola Tesla");
society.members = null;
// evaluates to null - does not throw a NullPointerException
list = (List<Inventor>) parser.parseExpression(expression)
.getValue(context);
assertThat(list).isNull();
}
@Test
@SuppressWarnings("unchecked")
void nullSafeSelectFirst() {
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression = "members?.^[nationality == 'Serbian' || nationality == 'Idvor']"; // <1>
// evaluates to Inventor("Nikola Tesla")
Inventor inventor = parser.parseExpression(expression)
.getValue(context, Inventor.class);
assertThat(inventor).extracting(Inventor::getName).isEqualTo("Nikola Tesla");
society.members = null;
// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
.getValue(context, Inventor.class);
assertThat(inventor).isNull();
}
@Test
@SuppressWarnings("unchecked")
void nullSafeSelectLast() {
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression = "members?.$[nationality == 'Serbian' || nationality == 'Idvor']"; // <1>
// evaluates to Inventor("Pupin")
Inventor inventor = parser.parseExpression(expression)
.getValue(context, Inventor.class);
assertThat(inventor).extracting(Inventor::getName).isEqualTo("Pupin");
society.members = null;
// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
.getValue(context, Inventor.class);
assertThat(inventor).isNull();
}
@Test
@SuppressWarnings("unchecked")
void nullSafeProjection() {
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
// evaluates to ["Smiljan", "Idvor"]
List placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1>
.getValue(context, List.class);
assertThat(placesOfBirth).containsExactly("Smiljan", "Idvor");
society.members = null;
// evaluates to null - does not throw a NullPointerException
placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2>
.getValue(context, List.class);
assertThat(placesOfBirth).isNull();
}
}
@Nested

Loading…
Cancel
Save