From 52b8c71dcde583185915875e7aa56fc206672a2e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:05:48 +0100 Subject: [PATCH] Retain null-safe syntax in AST representation of selection & projection Prior to this commit, SpEL's CompoundExpression omitted the null-safe syntax in AST string representations of the selection and projection operators. To address this, this commit implements isNullSafe() in Projection and Selection. Closes gh-32515 --- .../expression/spel/ast/Projection.java | 9 ++++ .../expression/spel/ast/Selection.java | 9 ++++ .../expression/spel/ParsingTests.java | 44 ++++++++++++++----- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index 4c87dd05203..121b7245ae9 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -52,6 +52,15 @@ public class Projection extends SpelNodeImpl { } + /** + * Does this node represent a null-safe projection operation? + * @since 6.1.6 + */ + @Override + public final boolean isNullSafe() { + return this.nullSafe; + } + @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { return getValueRef(state).getValue(); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index 374a9da4d93..2593998d84d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -78,6 +78,15 @@ public class Selection extends SpelNodeImpl { } + /** + * Does this node represent a null-safe selection operation? + * @since 6.1.6 + */ + @Override + public final boolean isNullSafe() { + return this.nullSafe; + } + @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { return getValueRef(state).getValue(); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java index 3f9df2ca891..4eebccab494 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -44,9 +44,24 @@ class ParsingTests { @Test void compoundExpressions() { + parseCheck("#var1.methodOne().methodTwo(42)"); + parseCheck("#func1().methodOne().methodTwo(42)"); + parseCheck("#func2('enigma').methodOne().methodTwo(42)"); parseCheck("property1.property2.methodOne()"); - parseCheck("property1[0].property2['key'].methodOne()"); + parseCheck("property1.methodOne('enigma').methodTwo(42)"); + parseCheck("property1.methodOne().property2.methodTwo()"); + parseCheck("property1[0].property2['key'].methodTwo()"); + parseCheck("property1[0][1].property2['key'][42].methodTwo()"); + + // null-safe variants + parseCheck("#var1?.methodOne()?.methodTwo(42)"); + parseCheck("#func1()?.methodOne()?.methodTwo(42)"); + parseCheck("#func2('enigma')?.methodOne()?.methodTwo(42)"); + parseCheck("property1?.property2?.methodOne()"); + parseCheck("property1?.methodOne('enigma')?.methodTwo(42)"); parseCheck("property1?.methodOne()?.property2?.methodTwo()"); + parseCheck("property1[0]?.property2['key']?.methodTwo()"); + parseCheck("property1[0][1]?.property2['key'][42]?.methodTwo()"); } @Test @@ -132,25 +147,34 @@ class ParsingTests { @Test void projection() { - parseCheck("{1,2,3,4,5,6,7,8,9,10}.![#isEven()]"); + parseCheck("{1,2,3}.![#isEven()]"); + + // null-safe variant + parseCheck("{1,2,3}?.![#isEven()]"); } @Test void selection() { - parseCheck("{1,2,3,4,5,6,7,8,9,10}.?[#isEven(#this) == 'y']", - "{1,2,3,4,5,6,7,8,9,10}.?[(#isEven(#this) == 'y')]"); + parseCheck("{1,2,3}.?[#isEven(#this)]"); + + // null-safe variant + parseCheck("{1,2,3}?.?[#isEven(#this)]"); } @Test - void selectionFirst() { - parseCheck("{1,2,3,4,5,6,7,8,9,10}.^[#isEven(#this) == 'y']", - "{1,2,3,4,5,6,7,8,9,10}.^[(#isEven(#this) == 'y')]"); + void selectFirst() { + parseCheck("{1,2,3}.^[#isEven(#this)]"); + + // null-safe variant + parseCheck("{1,2,3}?.^[#isEven(#this)]"); } @Test - void selectionLast() { - parseCheck("{1,2,3,4,5,6,7,8,9,10}.$[#isEven(#this) == 'y']", - "{1,2,3,4,5,6,7,8,9,10}.$[(#isEven(#this) == 'y')]"); + void selectLast() { + parseCheck("{1,2,3}.$[#isEven(#this)]"); + + // null-safe variant + parseCheck("{1,2,3}?.$[#isEven(#this)]"); } }