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
of a projection across a map is a list that consists of the evaluation of the projection of a projection across a map is a list that consists of the evaluation of the projection
expression against each map entry. 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,
syntax is `.^[selectionExpression]`. To obtain the last element matching the selection syntax is `.^[selectionExpression]`. To obtain the last element matching the selection
expression, the syntax is `.$[selectionExpression]`. 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
that it is not `null` before accessing methods or properties of the object. To avoid 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. 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 The following example shows how to use the safe navigation operator for property access
(`?.`). (`?.`).
@ -59,3 +62,224 @@ Kotlin::
<2> Use safe navigation operator on null `placeOfBirth` property <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 {
.getValue(context, tesla, String.class); .getValue(context, tesla, String.class);
assertThat(city).isNull(); 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 @Nested

Loading…
Cancel
Save