diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc index 5c16426a3623..78a3ec21fe4f 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc +++ b/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 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. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc index f0f70ffad9ac..b87bc1733413 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc +++ b/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 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. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc index 22a9aad4e62c..9974b027db41 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc +++ b/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 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:: <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 list = (List) parser.parseExpression(expression) + .getValue(context); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + list = (List) 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 + + society.members = null + + // evaluates to null - does not throw a NullPointerException + list = parser.parseExpression(expression) + .getValue(context) as List +---- +<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 +====== + + diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index 4456208c42fd..14d9d60e1987 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -648,6 +648,85 @@ void nullSafePropertyAccess() { .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 list = (List) 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) 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