From 9f4d46fe3374cc1e2ca8b2616107680432650a2e Mon Sep 17 00:00:00 2001 From: Grigory Stepanov Date: Wed, 18 Jan 2023 20:14:06 +0300 Subject: [PATCH] Introduce null-safe index operator in SpEL See gh-29847 --- .../expression/spel/ast/Indexer.java | 15 +++++++------ .../InternalSpelExpressionParser.java | 12 ++++++----- .../expression/spel/IndexingTests.java | 21 +++++++++++++++++++ 3 files changed, 37 insertions(+), 11 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 723e80b1ed..1477724b1b 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 @@ -103,12 +103,12 @@ public class Indexer extends SpelNodeImpl { private PropertyAccessor cachedWriteAccessor; - /** - * Create an {@code Indexer} with the given start position, end position, and - * index expression. - */ - public Indexer(int startPos, int endPos, SpelNodeImpl indexExpression) { - super(startPos, endPos, indexExpression); + private final boolean nullSafe; + + + public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expr) { + super(startPos, endPos, expr); + this.nullSafe = nullSafe; } @@ -161,6 +161,9 @@ public class Indexer extends SpelNodeImpl { // 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); } 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 33af0c254f..c998c350f1 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 @@ -399,7 +399,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { @Nullable private SpelNodeImpl eatNonDottedNode() { if (peekToken(TokenKind.LSQUARE)) { - if (maybeEatIndexer()) { + if (maybeEatIndexer(false)) { return pop(); } } @@ -419,7 +419,8 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { 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) { @@ -537,7 +538,8 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { 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()) { @@ -699,7 +701,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { return true; } - private boolean maybeEatIndexer() { + private boolean maybeEatIndexer(boolean nullSafeNavigation) { Token t = peekToken(); if (t == null || !peekToken(TokenKind.LSQUARE, true)) { return false; @@ -709,7 +711,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { throw internalException(t.startPos, SpelMessage.MISSING_SELECTION_EXPRESSION); } 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 5ee8a2d2b0..af57312a9b 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 @@ -376,6 +376,20 @@ class IndexingTests { assertThat(expression.getValue(this, String.class)).isEqualTo("apple"); } + @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(); + } + @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @@ -436,4 +450,11 @@ class IndexingTests { } + + static class ContextWithNullCollections { + public List nullList = null; + public String[] nullArray = null; + public Map nullMap = null; + } + }