Skip to content

Commit 4a5dc7c

Browse files
committed
Document null-safe collection selection/projection support in SpEL
Closes gh-32208
1 parent 347d085 commit 4a5dc7c

File tree

4 files changed

+317
-0
lines changed

4 files changed

+317
-0
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,12 @@ evaluated against each entry in the map (represented as a Java `Map.Entry`). The
3434
of a projection across a map is a list that consists of the evaluation of the projection
3535
expression against each map entry.
3636

37+
[NOTE]
38+
====
39+
The Spring Expression Language also supports safe navigation for collection projection.
3740
41+
See
42+
xref:core/expressions/language-ref/operator-safe-navigation.adoc#expressions-operator-safe-navigation-selection-and-projection[Safe Collection Selection and Projection]
43+
for details.
44+
====
3845

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,12 @@ the last element. To obtain the first element matching the selection expression,
5959
syntax is `.^[selectionExpression]`. To obtain the last element matching the selection
6060
expression, the syntax is `.$[selectionExpression]`.
6161

62+
[NOTE]
63+
====
64+
The Spring Expression Language also supports safe navigation for collection selection.
6265
66+
See
67+
xref:core/expressions/language-ref/operator-safe-navigation.adoc#expressions-operator-safe-navigation-selection-and-projection[Safe Collection Selection and Projection]
68+
for details.
69+
====
6370

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

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ language. Typically, when you have a reference to an object, you might need to v
77
that it is not `null` before accessing methods or properties of the object. To avoid
88
this, the safe navigation operator returns `null` instead of throwing an exception.
99

10+
[[expressions-operator-safe-navigation-property-access]]
11+
== Safe Property and Method Access
12+
1013
The following example shows how to use the safe navigation operator for property access
1114
(`?.`).
1215

@@ -59,3 +62,224 @@ Kotlin::
5962
<2> Use safe navigation operator on null `placeOfBirth` property
6063
======
6164

65+
[NOTE]
66+
====
67+
The safe navigation operator also applies to method invocations on an object.
68+
69+
For example, the expression `#calculator?.max(4, 2)` evaluates to `null` if the
70+
`#calculator` variable has not been configured in the context. Otherwise, the
71+
`max(int, int)` method will be invoked on the `#calculator`.
72+
====
73+
74+
75+
[[expressions-operator-safe-navigation-selection-and-projection]]
76+
== Safe Collection Selection and Projection
77+
78+
The Spring Expression Language supports safe navigation for
79+
xref:core/expressions/language-ref/collection-selection.adoc[collection selection] and
80+
xref:core/expressions/language-ref/collection-projection.adoc[collection projection] via
81+
the following operators.
82+
83+
* null-safe selection: `?.?`
84+
* null-safe select first: `?.^`
85+
* null-safe select last: `?.$`
86+
* null-safe projection: `?.!`
87+
88+
The following example shows how to use the safe navigation operator for collection
89+
selection (`?.?`).
90+
91+
[tabs]
92+
======
93+
Java::
94+
+
95+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
96+
----
97+
ExpressionParser parser = new SpelExpressionParser();
98+
IEEE society = new IEEE();
99+
StandardEvaluationContext context = new StandardEvaluationContext(society);
100+
String expression = "members?.?[nationality == 'Serbian']"; // <1>
101+
102+
// evaluates to [Inventor("Nikola Tesla")]
103+
List<Inventor> list = (List<Inventor>) parser.parseExpression(expression)
104+
.getValue(context);
105+
106+
society.members = null;
107+
108+
// evaluates to null - does not throw a NullPointerException
109+
list = (List<Inventor>) parser.parseExpression(expression)
110+
.getValue(context);
111+
----
112+
<1> Use null-safe selection operator on potentially null `members` list
113+
114+
Kotlin::
115+
+
116+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
117+
----
118+
val parser = SpelExpressionParser()
119+
val society = IEEE()
120+
val context = StandardEvaluationContext(society)
121+
val expression = "members?.?[nationality == 'Serbian']" // <1>
122+
123+
// evaluates to [Inventor("Nikola Tesla")]
124+
var list = parser.parseExpression(expression)
125+
.getValue(context) as List<Inventor>
126+
127+
society.members = null
128+
129+
// evaluates to null - does not throw a NullPointerException
130+
list = parser.parseExpression(expression)
131+
.getValue(context) as List<Inventor>
132+
----
133+
<1> Use null-safe selection operator on potentially null `members` list
134+
======
135+
136+
The following example shows how to use the "null-safe select first" operator for
137+
collections (`?.^`).
138+
139+
[tabs]
140+
======
141+
Java::
142+
+
143+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
144+
----
145+
ExpressionParser parser = new SpelExpressionParser();
146+
IEEE society = new IEEE();
147+
StandardEvaluationContext context = new StandardEvaluationContext(society);
148+
String expression =
149+
"members?.^[nationality == 'Serbian' || nationality == 'Idvor']"; // <1>
150+
151+
// evaluates to Inventor("Nikola Tesla")
152+
Inventor inventor = parser.parseExpression(expression)
153+
.getValue(context, Inventor.class);
154+
155+
society.members = null;
156+
157+
// evaluates to null - does not throw a NullPointerException
158+
inventor = parser.parseExpression(expression)
159+
.getValue(context, Inventor.class);
160+
----
161+
<1> Use "null-safe select first" operator on potentially null `members` list
162+
163+
Kotlin::
164+
+
165+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
166+
----
167+
val parser = SpelExpressionParser()
168+
val society = IEEE()
169+
val context = StandardEvaluationContext(society)
170+
val expression =
171+
"members?.^[nationality == 'Serbian' || nationality == 'Idvor']" // <1>
172+
173+
// evaluates to Inventor("Nikola Tesla")
174+
var inventor = parser.parseExpression(expression)
175+
.getValue(context, Inventor::class.java)
176+
177+
society.members = null
178+
179+
// evaluates to null - does not throw a NullPointerException
180+
inventor = parser.parseExpression(expression)
181+
.getValue(context, Inventor::class.java)
182+
----
183+
<1> Use "null-safe select first" operator on potentially null `members` list
184+
======
185+
186+
187+
The following example shows how to use the "null-safe select last" operator for
188+
collections (`?.$`).
189+
190+
[tabs]
191+
======
192+
Java::
193+
+
194+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
195+
----
196+
ExpressionParser parser = new SpelExpressionParser();
197+
IEEE society = new IEEE();
198+
StandardEvaluationContext context = new StandardEvaluationContext(society);
199+
String expression =
200+
"members?.$[nationality == 'Serbian' || nationality == 'Idvor']"; // <1>
201+
202+
// evaluates to Inventor("Pupin")
203+
Inventor inventor = parser.parseExpression(expression)
204+
.getValue(context, Inventor.class);
205+
206+
society.members = null;
207+
208+
// evaluates to null - does not throw a NullPointerException
209+
inventor = parser.parseExpression(expression)
210+
.getValue(context, Inventor.class);
211+
----
212+
<1> Use "null-safe select last" operator on potentially null `members` list
213+
214+
Kotlin::
215+
+
216+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
217+
----
218+
val parser = SpelExpressionParser()
219+
val society = IEEE()
220+
val context = StandardEvaluationContext(society)
221+
val expression =
222+
"members?.$[nationality == 'Serbian' || nationality == 'Idvor']" // <1>
223+
224+
// evaluates to Inventor("Pupin")
225+
var inventor = parser.parseExpression(expression)
226+
.getValue(context, Inventor::class.java)
227+
228+
society.members = null
229+
230+
// evaluates to null - does not throw a NullPointerException
231+
inventor = parser.parseExpression(expression)
232+
.getValue(context, Inventor::class.java)
233+
----
234+
<1> Use "null-safe select last" operator on potentially null `members` list
235+
======
236+
237+
The following example shows how to use the safe navigation operator for collection
238+
projection (`?.!`).
239+
240+
[tabs]
241+
======
242+
Java::
243+
+
244+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
245+
----
246+
ExpressionParser parser = new SpelExpressionParser();
247+
IEEE society = new IEEE();
248+
StandardEvaluationContext context = new StandardEvaluationContext(society);
249+
250+
// evaluates to ["Smiljan", "Idvor"]
251+
List placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1>
252+
.getValue(context, List.class);
253+
254+
society.members = null;
255+
256+
// evaluates to null - does not throw a NullPointerException
257+
placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2>
258+
.getValue(context, List.class);
259+
----
260+
<1> Use null-safe projection operator on non-null `members` list
261+
<2> Use null-safe projection operator on null `members` list
262+
263+
Kotlin::
264+
+
265+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
266+
----
267+
val parser = SpelExpressionParser()
268+
val society = IEEE()
269+
val context = StandardEvaluationContext(society)
270+
271+
// evaluates to ["Smiljan", "Idvor"]
272+
var placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1>
273+
.getValue(context, List::class.java)
274+
275+
society.members = null
276+
277+
// evaluates to null - does not throw a NullPointerException
278+
placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2>
279+
.getValue(context, List::class.java)
280+
----
281+
<1> Use null-safe projection operator on non-null `members` list
282+
<2> Use null-safe projection operator on null `members` list
283+
======
284+
285+

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,85 @@ void nullSafePropertyAccess() {
648648
.getValue(context, tesla, String.class);
649649
assertThat(city).isNull();
650650
}
651+
652+
@Test
653+
@SuppressWarnings("unchecked")
654+
void nullSafeSelection() {
655+
IEEE society = new IEEE();
656+
StandardEvaluationContext context = new StandardEvaluationContext(society);
657+
String expression = "members?.?[nationality == 'Serbian']"; // <1>
658+
659+
// evaluates to [Inventor("Nikola Tesla")]
660+
List<Inventor> list = (List<Inventor>) parser.parseExpression(expression)
661+
.getValue(context);
662+
assertThat(list).map(Inventor::getName).containsOnly("Nikola Tesla");
663+
664+
society.members = null;
665+
666+
// evaluates to null - does not throw a NullPointerException
667+
list = (List<Inventor>) parser.parseExpression(expression)
668+
.getValue(context);
669+
assertThat(list).isNull();
670+
}
671+
672+
@Test
673+
@SuppressWarnings("unchecked")
674+
void nullSafeSelectFirst() {
675+
IEEE society = new IEEE();
676+
StandardEvaluationContext context = new StandardEvaluationContext(society);
677+
String expression = "members?.^[nationality == 'Serbian' || nationality == 'Idvor']"; // <1>
678+
679+
// evaluates to Inventor("Nikola Tesla")
680+
Inventor inventor = parser.parseExpression(expression)
681+
.getValue(context, Inventor.class);
682+
assertThat(inventor).extracting(Inventor::getName).isEqualTo("Nikola Tesla");
683+
684+
society.members = null;
685+
686+
// evaluates to null - does not throw a NullPointerException
687+
inventor = parser.parseExpression(expression)
688+
.getValue(context, Inventor.class);
689+
assertThat(inventor).isNull();
690+
}
691+
692+
@Test
693+
@SuppressWarnings("unchecked")
694+
void nullSafeSelectLast() {
695+
IEEE society = new IEEE();
696+
StandardEvaluationContext context = new StandardEvaluationContext(society);
697+
String expression = "members?.$[nationality == 'Serbian' || nationality == 'Idvor']"; // <1>
698+
699+
// evaluates to Inventor("Pupin")
700+
Inventor inventor = parser.parseExpression(expression)
701+
.getValue(context, Inventor.class);
702+
assertThat(inventor).extracting(Inventor::getName).isEqualTo("Pupin");
703+
704+
society.members = null;
705+
706+
// evaluates to null - does not throw a NullPointerException
707+
inventor = parser.parseExpression(expression)
708+
.getValue(context, Inventor.class);
709+
assertThat(inventor).isNull();
710+
}
711+
712+
@Test
713+
@SuppressWarnings("unchecked")
714+
void nullSafeProjection() {
715+
IEEE society = new IEEE();
716+
StandardEvaluationContext context = new StandardEvaluationContext(society);
717+
718+
// evaluates to ["Smiljan", "Idvor"]
719+
List placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1>
720+
.getValue(context, List.class);
721+
assertThat(placesOfBirth).containsExactly("Smiljan", "Idvor");
722+
723+
society.members = null;
724+
725+
// evaluates to null - does not throw a NullPointerException
726+
placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2>
727+
.getValue(context, List.class);
728+
assertThat(placesOfBirth).isNull();
729+
}
651730
}
652731

653732
@Nested

0 commit comments

Comments
 (0)