Skip to content

Commit 9f4d46f

Browse files
Trympyrymsbrannen
authored andcommitted
Introduce null-safe index operator in SpEL
See spring-projectsgh-29847
1 parent 2a1abb5 commit 9f4d46f

File tree

3 files changed

+37
-11
lines changed

3 files changed

+37
-11
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java

+9-6
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT}
103103
private PropertyAccessor cachedWriteAccessor;
104104

105105

106-
/**
107-
* Create an {@code Indexer} with the given start position, end position, and
108-
* index expression.
109-
*/
110-
public Indexer(int startPos, int endPos, SpelNodeImpl indexExpression) {
111-
super(startPos, endPos, indexExpression);
106+
private final boolean nullSafe;
107+
108+
109+
public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expr) {
110+
super(startPos, endPos, expr);
111+
this.nullSafe = nullSafe;
112112
}
113113

114114

@@ -161,6 +161,9 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException
161161

162162
// Raise a proper exception in case of a null target
163163
if (target == null) {
164+
if (this.nullSafe) {
165+
return ValueRef.NullValueRef.INSTANCE;
166+
}
164167
throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE);
165168
}
166169

spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java

+7-5
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ private SpelNodeImpl eatNode() {
399399
@Nullable
400400
private SpelNodeImpl eatNonDottedNode() {
401401
if (peekToken(TokenKind.LSQUARE)) {
402-
if (maybeEatIndexer()) {
402+
if (maybeEatIndexer(false)) {
403403
return pop();
404404
}
405405
}
@@ -419,7 +419,8 @@ private SpelNodeImpl eatDottedNode() {
419419
Token t = takeToken(); // it was a '.' or a '?.'
420420
boolean nullSafeNavigation = (t.kind == TokenKind.SAFE_NAVI);
421421
if (maybeEatMethodOrProperty(nullSafeNavigation) || maybeEatFunctionOrVar() ||
422-
maybeEatProjection(nullSafeNavigation) || maybeEatSelection(nullSafeNavigation)) {
422+
maybeEatProjection(nullSafeNavigation) || maybeEatSelection(nullSafeNavigation) ||
423+
maybeEatIndexer(nullSafeNavigation)) {
423424
return pop();
424425
}
425426
if (peekToken() == null) {
@@ -537,7 +538,8 @@ else if (maybeEatTypeReference() || maybeEatNullReference() || maybeEatConstruct
537538
else if (maybeEatBeanReference()) {
538539
return pop();
539540
}
540-
else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer()) {
541+
else if (maybeEatProjection(false) || maybeEatSelection(false) ||
542+
maybeEatIndexer(false)) {
541543
return pop();
542544
}
543545
else if (maybeEatInlineListOrMap()) {
@@ -699,7 +701,7 @@ else if (peekToken(TokenKind.COLON, true)) { // map!
699701
return true;
700702
}
701703

702-
private boolean maybeEatIndexer() {
704+
private boolean maybeEatIndexer(boolean nullSafeNavigation) {
703705
Token t = peekToken();
704706
if (t == null || !peekToken(TokenKind.LSQUARE, true)) {
705707
return false;
@@ -709,7 +711,7 @@ private boolean maybeEatIndexer() {
709711
throw internalException(t.startPos, SpelMessage.MISSING_SELECTION_EXPRESSION);
710712
}
711713
eatToken(TokenKind.RSQUARE);
712-
this.constructedNodes.push(new Indexer(t.startPos, t.endPos, expr));
714+
this.constructedNodes.push(new Indexer(nullSafeNavigation, t.startPos, t.endPos, expr));
713715
return true;
714716
}
715717

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

+21
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,20 @@ void listOfMaps() {
376376
assertThat(expression.getValue(this, String.class)).isEqualTo("apple");
377377
}
378378

379+
@Test
380+
void nullSafeIndex() {
381+
ContextWithNullCollections testContext = new ContextWithNullCollections();
382+
StandardEvaluationContext context = new StandardEvaluationContext(testContext);
383+
Expression expr = new SpelExpressionParser().parseRaw("nullList?.[4]");
384+
assertThat(expr.getValue(context)).isNull();
385+
386+
expr = new SpelExpressionParser().parseRaw("nullArray?.[4]");
387+
assertThat(expr.getValue(context)).isNull();
388+
389+
expr = new SpelExpressionParser().parseRaw("nullMap:?.[4]");
390+
assertThat(expr.getValue(context)).isNull();
391+
}
392+
379393

380394
@Target({ElementType.FIELD})
381395
@Retention(RetentionPolicy.RUNTIME)
@@ -436,4 +450,11 @@ public Class<?>[] getSpecificTargetClasses() {
436450

437451
}
438452

453+
454+
static class ContextWithNullCollections {
455+
public List nullList = null;
456+
public String[] nullArray = null;
457+
public Map nullMap = null;
458+
}
459+
439460
}

0 commit comments

Comments
 (0)