From dd4897cb88ea523e2bbb11a2e22ac80600577aa2 Mon Sep 17 00:00:00 2001 From: Grigory Stepanov Date: Wed, 18 Jan 2023 20:14:06 +0300 Subject: [PATCH] SpEL: add support for null-safe get element from collection by index --- .../expression/spel/ast/Indexer.java | 8 +++++++- .../InternalSpelExpressionParser.java | 12 +++++++----- .../expression/spel/IndexingTests.java | 19 +++++++++++++++++++ .../expression/spel/SpelReproTests.java | 10 ++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index a12c11df85e6..23a70cb55e0e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -91,9 +91,12 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} @Nullable private IndexedType indexedType; + private final boolean nullSafe; - public Indexer(int startPos, int endPos, SpelNodeImpl expr) { + + public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expr) { super(startPos, endPos, expr); + this.nullSafe = nullSafe; } @@ -142,6 +145,9 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException // Raise a proper exception in case of a null target if (target == null) { + if (this.nullSafe) { + return ValueRef.NullValueRef.INSTANCE; + } throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); } // At this point, we need a TypeDescriptor for a non-null target object diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 33c4d9db4eb3..db83520448fd 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -375,7 +375,7 @@ private SpelNodeImpl eatNode() { @Nullable private SpelNodeImpl eatNonDottedNode() { if (peekToken(TokenKind.LSQUARE)) { - if (maybeEatIndexer()) { + if (maybeEatIndexer(false)) { return pop(); } } @@ -395,7 +395,8 @@ private SpelNodeImpl eatDottedNode() { Token t = takeToken(); // it was a '.' or a '?.' boolean nullSafeNavigation = (t.kind == TokenKind.SAFE_NAVI); if (maybeEatMethodOrProperty(nullSafeNavigation) || maybeEatFunctionOrVar() || - maybeEatProjection(nullSafeNavigation) || maybeEatSelection(nullSafeNavigation)) { + maybeEatProjection(nullSafeNavigation) || maybeEatSelection(nullSafeNavigation) || + maybeEatIndexer(nullSafeNavigation)) { return pop(); } if (peekToken() == null) { @@ -513,7 +514,8 @@ else if (maybeEatTypeReference() || maybeEatNullReference() || maybeEatConstruct else if (maybeEatBeanReference()) { return pop(); } - else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer()) { + else if (maybeEatProjection(false) || maybeEatSelection(false) || + maybeEatIndexer(false)) { return pop(); } else if (maybeEatInlineListOrMap()) { @@ -678,7 +680,7 @@ else if (peekToken(TokenKind.COLON, true)) { // map! return true; } - private boolean maybeEatIndexer() { + private boolean maybeEatIndexer(boolean nullSafeNavigation) { Token t = peekToken(); if (!peekToken(TokenKind.LSQUARE, true)) { return false; @@ -687,7 +689,7 @@ private boolean maybeEatIndexer() { SpelNodeImpl expr = eatExpression(); Assert.state(expr != null, "No node"); eatToken(TokenKind.RSQUARE); - this.constructedNodes.push(new Indexer(t.startPos, t.endPos, expr)); + this.constructedNodes.push(new Indexer(nullSafeNavigation, t.startPos, t.endPos, expr)); return true; } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java index 6afcf60d1f18..ef89ac40fdab 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java @@ -409,4 +409,23 @@ void testListsOfMap() { public List listOfMapsNotGeneric; + @Test + void nullSafeIndex() { + ContextWithNullCollections testContext = new ContextWithNullCollections(); + StandardEvaluationContext context = new StandardEvaluationContext(testContext); + Expression expr = new SpelExpressionParser().parseRaw("nullList?.[4]"); + assertThat(expr.getValue(context)).isNull(); + + expr = new SpelExpressionParser().parseRaw("nullArray?.[4]"); + assertThat(expr.getValue(context)).isNull(); + + expr = new SpelExpressionParser().parseRaw("nullMap:?.[4]"); + assertThat(expr.getValue(context)).isNull(); + } + + public static class ContextWithNullCollections { + public List nullList = null; + public String[] nullArray = null; + public Map nullMap = null; + } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java index d7ba60c704bb..ea19a686bdc1 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -155,6 +155,16 @@ void SPR5804() { assertThat(expr.getValue(context)).isEqualTo("bar"); } + @Test + void SPR16929() { + C testContext = new C(); + testContext.ls = null; + StandardEvaluationContext context = new StandardEvaluationContext(testContext); + context.addPropertyAccessor(new MapAccessor()); + Expression expr = new SpelExpressionParser().parseRaw("ls?.[4]"); + assertThat(expr.getValue(context)).isNull(); + } + @Test void SPR5847() { StandardEvaluationContext context = new StandardEvaluationContext(new TestProperties());