Introduce null-safe index operator in SpEL

See gh-29847
This commit is contained in:
Grigory Stepanov 2023-01-18 20:14:06 +03:00 committed by Sam Brannen
parent 2a1abb5553
commit 9f4d46fe33
3 changed files with 37 additions and 11 deletions

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}
}