Skip to content

Commit 153d1bc

Browse files
committed
Support Collection target types in custom IndexAccessors
Prior to this commit, an IndexAccessor could not provide support for a Collection target type, since the built-in support for indexing into a Collection in SpEL's Indexer took precedence. This commit allows an IndexAccessor to support custom Collection target types by separating the built-in List and Collection support and applying the built-in Collection support after custom index accessors have been applied. Closes gh-32736
1 parent 4e6591e commit 153d1bc

File tree

2 files changed

+84
-20
lines changed

2 files changed

+84
-20
lines changed

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

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,22 @@ private ValueRef getValueRef(ExpressionState state, AccessMode accessMode) throw
212212
// At this point, we need a TypeDescriptor for a non-null target object
213213
Assert.state(targetDescriptor != null, "No type descriptor");
214214

215+
// Indexing into an array
216+
if (target.getClass().isArray()) {
217+
int intIndex = convertIndexToInt(state, index);
218+
this.indexedType = IndexedType.ARRAY;
219+
return new ArrayIndexingValueRef(state.getTypeConverter(), target, intIndex, targetDescriptor);
220+
}
221+
222+
// Indexing into a List
223+
if (target instanceof List<?> list) {
224+
int intIndex = convertIndexToInt(state, index);
225+
this.indexedType = IndexedType.LIST;
226+
return new CollectionIndexingValueRef(list, intIndex, targetDescriptor,
227+
state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
228+
state.getConfiguration().getMaximumAutoGrowSize());
229+
}
230+
215231
// Indexing into a Map
216232
if (target instanceof Map<?, ?> map) {
217233
Object key = index;
@@ -223,26 +239,11 @@ private ValueRef getValueRef(ExpressionState state, AccessMode accessMode) throw
223239
return new MapIndexingValueRef(state.getTypeConverter(), map, key, targetDescriptor);
224240
}
225241

226-
// If the object is something that looks indexable by an integer,
227-
// attempt to treat the index value as a number
228-
if (target.getClass().isArray() || target instanceof Collection || target instanceof String) {
229-
int idx = (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class));
230-
if (target.getClass().isArray()) {
231-
this.indexedType = IndexedType.ARRAY;
232-
return new ArrayIndexingValueRef(state.getTypeConverter(), target, idx, targetDescriptor);
233-
}
234-
else if (target instanceof Collection<?> collection) {
235-
if (target instanceof List) {
236-
this.indexedType = IndexedType.LIST;
237-
}
238-
return new CollectionIndexingValueRef(collection, idx, targetDescriptor,
239-
state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
240-
state.getConfiguration().getMaximumAutoGrowSize());
241-
}
242-
else {
243-
this.indexedType = IndexedType.STRING;
244-
return new StringIndexingValueRef((String) target, idx, targetDescriptor);
245-
}
242+
// Indexing into a String
243+
if (target instanceof String string) {
244+
int intIndex = convertIndexToInt(state, index);
245+
this.indexedType = IndexedType.STRING;
246+
return new StringIndexingValueRef(string, intIndex, targetDescriptor);
246247
}
247248

248249
// Check for a custom IndexAccessor.
@@ -280,6 +281,14 @@ else if (target instanceof Collection<?> collection) {
280281
}
281282
}
282283

284+
// Fallback indexing support for collections
285+
if (target instanceof Collection<?> collection) {
286+
int intIndex = convertIndexToInt(state, index);
287+
return new CollectionIndexingValueRef(collection, intIndex, targetDescriptor,
288+
state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
289+
state.getConfiguration().getMaximumAutoGrowSize());
290+
}
291+
283292
// As a last resort, try to treat the index value as a property of the context object.
284293
TypeDescriptor valueType = indexValue.getTypeDescriptor();
285294
if (valueType != null && String.class == valueType.getType()) {
@@ -458,6 +467,10 @@ private void setExitTypeDescriptor(String descriptor) {
458467
}
459468
}
460469

470+
private static int convertIndexToInt(ExpressionState state, Object index) {
471+
return (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class));
472+
}
473+
461474
private static Class<?> getObjectType(Object obj) {
462475
return (obj instanceof Class<?> clazz ? clazz : obj.getClass());
463476
}

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323
import java.math.BigDecimal;
24+
import java.util.AbstractCollection;
2425
import java.util.ArrayList;
2526
import java.util.Arrays;
27+
import java.util.Collection;
2628
import java.util.HashMap;
29+
import java.util.Iterator;
2730
import java.util.List;
2831
import java.util.Map;
2932
import java.util.NoSuchElementException;
@@ -44,6 +47,7 @@
4447
import org.springframework.expression.PropertyAccessor;
4548
import org.springframework.expression.TypedValue;
4649
import org.springframework.expression.spel.standard.SpelExpressionParser;
50+
import org.springframework.expression.spel.support.ReflectiveIndexAccessor;
4751
import org.springframework.expression.spel.support.SimpleEvaluationContext;
4852
import org.springframework.expression.spel.support.StandardEvaluationContext;
4953
import org.springframework.expression.spel.testresources.Person;
@@ -735,6 +739,25 @@ void readIndexWithStringIndexType() {
735739
.havingCause().withMessage("unknown bird: property");
736740
}
737741

742+
@Test // gh-32736
743+
void readIndexWithCollectionTargetType() {
744+
context.addIndexAccessor(new ColorCollectionIndexAccessor());
745+
746+
Expression expression = parser.parseExpression("[0]");
747+
748+
// List.of() relies on built-in list support.
749+
assertThat(expression.getValue(context, List.of(Color.RED))).isEqualTo(Color.RED);
750+
751+
ColorCollection colorCollection = new ColorCollection();
752+
753+
// Preconditions for this use case.
754+
assertThat(colorCollection).isInstanceOf(Collection.class);
755+
assertThat(colorCollection).isNotInstanceOf(List.class);
756+
757+
// ColorCollection relies on custom ColorCollectionIndexAccessor.
758+
assertThat(expression.getValue(context, colorCollection)).isEqualTo(Color.RED);
759+
}
760+
738761
static class BirdNameToColorMappings {
739762

740763
public final String property = "enigma";
@@ -755,6 +778,34 @@ static class BirdNameToColorMappingsIndexAccessor extends ReflectiveIndexAccesso
755778
}
756779
}
757780

781+
static class ColorCollection extends AbstractCollection<Color> {
782+
783+
public Color get(int index) {
784+
return switch (index) {
785+
case 0 -> Color.RED;
786+
case 1 -> Color.BLUE;
787+
default -> throw new NoSuchElementException("No color at index " + index);
788+
};
789+
}
790+
791+
@Override
792+
public Iterator<Color> iterator() {
793+
throw new UnsupportedOperationException();
794+
}
795+
796+
@Override
797+
public int size() {
798+
throw new UnsupportedOperationException();
799+
}
800+
}
801+
802+
static class ColorCollectionIndexAccessor extends ReflectiveIndexAccessor {
803+
804+
ColorCollectionIndexAccessor() {
805+
super(ColorCollection.class, int.class, "get");
806+
}
807+
}
808+
758809
/**
759810
* {@link IndexAccessor} that knows how to read and write indexes in a
760811
* Jackson {@link ArrayNode}.

0 commit comments

Comments
 (0)