Skip to content

Commit 4d43317

Browse files
committed
Revise null-safe index operator support in SpEL
See gh-29847
1 parent 9f4d46f commit 4d43317

File tree

3 files changed

+98
-31
lines changed

3 files changed

+98
-31
lines changed

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

+28-12
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public class Indexer extends SpelNodeImpl {
6868
private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT}
6969

7070

71+
private final boolean nullSafe;
72+
7173
@Nullable
7274
private IndexedType indexedType;
7375

@@ -103,11 +105,24 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT}
103105
private PropertyAccessor cachedWriteAccessor;
104106

105107

106-
private final boolean nullSafe;
107-
108+
/**
109+
* Create an {@code Indexer} with the given start position, end position, and
110+
* index expression.
111+
* @see #Indexer(boolean, int, int, SpelNodeImpl)
112+
* @deprecated as of Spring Framework 6.2, in favor of {@link #Indexer(boolean, int, int, SpelNodeImpl)}
113+
*/
114+
@Deprecated(since = "6.2", forRemoval = true)
115+
public Indexer(int startPos, int endPos, SpelNodeImpl indexExpression) {
116+
this(false, startPos, endPos, indexExpression);
117+
}
108118

109-
public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expr) {
110-
super(startPos, endPos, expr);
119+
/**
120+
* Create an {@code Indexer} with the given null-safe flag, start position,
121+
* end position, and index expression.
122+
* @since 6.2
123+
*/
124+
public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl indexExpression) {
125+
super(startPos, endPos, indexExpression);
111126
this.nullSafe = nullSafe;
112127
}
113128

@@ -136,6 +151,15 @@ public boolean isWritable(ExpressionState expressionState) throws SpelEvaluation
136151
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
137152
TypedValue context = state.getActiveContextObject();
138153
Object target = context.getValue();
154+
155+
if (target == null) {
156+
if (this.nullSafe) {
157+
return ValueRef.NullValueRef.INSTANCE;
158+
}
159+
// Raise a proper exception in case of a null target
160+
throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE);
161+
}
162+
139163
TypeDescriptor targetDescriptor = context.getTypeDescriptor();
140164
TypedValue indexValue;
141165
Object index;
@@ -159,14 +183,6 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException
159183
}
160184
}
161185

162-
// Raise a proper exception in case of a null target
163-
if (target == null) {
164-
if (this.nullSafe) {
165-
return ValueRef.NullValueRef.INSTANCE;
166-
}
167-
throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE);
168-
}
169-
170186
// At this point, we need a TypeDescriptor for a non-null target object
171187
Assert.state(targetDescriptor != null, "No type descriptor");
172188

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,7 @@ else if (maybeEatTypeReference() || maybeEatNullReference() || maybeEatConstruct
538538
else if (maybeEatBeanReference()) {
539539
return pop();
540540
}
541-
else if (maybeEatProjection(false) || maybeEatSelection(false) ||
542-
maybeEatIndexer(false)) {
541+
else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer(false)) {
543542
return pop();
544543
}
545544
else if (maybeEatInlineListOrMap()) {

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

+69-17
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import java.util.HashMap;
2727
import java.util.List;
2828
import java.util.Map;
29+
import java.util.Set;
2930

31+
import org.junit.jupiter.api.Nested;
3032
import org.junit.jupiter.api.Test;
3133

3234
import org.springframework.expression.EvaluationContext;
@@ -35,6 +37,7 @@
3537
import org.springframework.expression.TypedValue;
3638
import org.springframework.expression.spel.standard.SpelExpressionParser;
3739
import org.springframework.expression.spel.support.StandardEvaluationContext;
40+
import org.springframework.expression.spel.testresources.Person;
3841

3942
import static org.assertj.core.api.Assertions.assertThat;
4043
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -376,18 +379,74 @@ void listOfMaps() {
376379
assertThat(expression.getValue(this, String.class)).isEqualTo("apple");
377380
}
378381

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();
382+
@Nested
383+
class NullSafeIndexTests { // gh-29847
384+
385+
private final RootContextWithIndexedProperties rootContext = new RootContextWithIndexedProperties();
386+
387+
private final StandardEvaluationContext context = new StandardEvaluationContext(rootContext);
385388

386-
expr = new SpelExpressionParser().parseRaw("nullArray?.[4]");
387-
assertThat(expr.getValue(context)).isNull();
389+
private final SpelExpressionParser parser = new SpelExpressionParser();
390+
391+
private Expression expression;
392+
393+
@Test
394+
void nullSafeIndexIntoArray() {
395+
expression = parser.parseExpression("array?.[0]");
396+
assertThat(expression.getValue(context)).isNull();
397+
rootContext.array = new int[] {42};
398+
assertThat(expression.getValue(context)).isEqualTo(42);
399+
}
400+
401+
@Test
402+
void nullSafeIndexIntoList() {
403+
expression = parser.parseExpression("list?.[0]");
404+
assertThat(expression.getValue(context)).isNull();
405+
rootContext.list = List.of(42);
406+
assertThat(expression.getValue(context)).isEqualTo(42);
407+
}
408+
409+
@Test
410+
void nullSafeIndexIntoSet() {
411+
expression = parser.parseExpression("set?.[0]");
412+
assertThat(expression.getValue(context)).isNull();
413+
rootContext.set = Set.of(42);
414+
assertThat(expression.getValue(context)).isEqualTo(42);
415+
}
416+
417+
@Test
418+
void nullSafeIndexIntoString() {
419+
expression = parser.parseExpression("string?.[0]");
420+
assertThat(expression.getValue(context)).isNull();
421+
rootContext.string = "XYZ";
422+
assertThat(expression.getValue(context)).isEqualTo("X");
423+
}
424+
425+
@Test
426+
void nullSafeIndexIntoMap() {
427+
expression = parser.parseExpression("map?.['enigma']");
428+
assertThat(expression.getValue(context)).isNull();
429+
rootContext.map = Map.of("enigma", 42);
430+
assertThat(expression.getValue(context)).isEqualTo(42);
431+
}
432+
433+
@Test
434+
void nullSafeIndexIntoObject() {
435+
expression = parser.parseExpression("person?.['name']");
436+
assertThat(expression.getValue(context)).isNull();
437+
rootContext.person = new Person("Jane");
438+
assertThat(expression.getValue(context)).isEqualTo("Jane");
439+
}
440+
441+
static class RootContextWithIndexedProperties {
442+
public int[] array;
443+
public List<Integer> list;
444+
public Set<Integer> set;
445+
public String string;
446+
public Map<String, Integer> map;
447+
public Person person;
448+
}
388449

389-
expr = new SpelExpressionParser().parseRaw("nullMap:?.[4]");
390-
assertThat(expr.getValue(context)).isNull();
391450
}
392451

393452

@@ -450,11 +509,4 @@ public Class<?>[] getSpecificTargetClasses() {
450509

451510
}
452511

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

0 commit comments

Comments
 (0)