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 287a4f5697..dd797c01ef 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 @@ -212,6 +212,22 @@ public class Indexer extends SpelNodeImpl { // At this point, we need a TypeDescriptor for a non-null target object Assert.state(targetDescriptor != null, "No type descriptor"); + // Indexing into an array + if (target.getClass().isArray()) { + int intIndex = convertIndexToInt(state, index); + this.indexedType = IndexedType.ARRAY; + return new ArrayIndexingValueRef(state.getTypeConverter(), target, intIndex, targetDescriptor); + } + + // Indexing into a List + if (target instanceof List list) { + int intIndex = convertIndexToInt(state, index); + this.indexedType = IndexedType.LIST; + return new CollectionIndexingValueRef(list, intIndex, targetDescriptor, + state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(), + state.getConfiguration().getMaximumAutoGrowSize()); + } + // Indexing into a Map if (target instanceof Map map) { Object key = index; @@ -223,26 +239,11 @@ public class Indexer extends SpelNodeImpl { return new MapIndexingValueRef(state.getTypeConverter(), map, key, targetDescriptor); } - // If the object is something that looks indexable by an integer, - // attempt to treat the index value as a number - if (target.getClass().isArray() || target instanceof Collection || target instanceof String) { - int idx = (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); - if (target.getClass().isArray()) { - this.indexedType = IndexedType.ARRAY; - return new ArrayIndexingValueRef(state.getTypeConverter(), target, idx, targetDescriptor); - } - else if (target instanceof Collection collection) { - if (target instanceof List) { - this.indexedType = IndexedType.LIST; - } - return new CollectionIndexingValueRef(collection, idx, targetDescriptor, - state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(), - state.getConfiguration().getMaximumAutoGrowSize()); - } - else { - this.indexedType = IndexedType.STRING; - return new StringIndexingValueRef((String) target, idx, targetDescriptor); - } + // Indexing into a String + if (target instanceof String string) { + int intIndex = convertIndexToInt(state, index); + this.indexedType = IndexedType.STRING; + return new StringIndexingValueRef(string, intIndex, targetDescriptor); } // Check for a custom IndexAccessor. @@ -280,6 +281,14 @@ public class Indexer extends SpelNodeImpl { } } + // Fallback indexing support for collections + if (target instanceof Collection collection) { + int intIndex = convertIndexToInt(state, index); + return new CollectionIndexingValueRef(collection, intIndex, targetDescriptor, + state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(), + state.getConfiguration().getMaximumAutoGrowSize()); + } + // As a last resort, try to treat the index value as a property of the context object. TypeDescriptor valueType = indexValue.getTypeDescriptor(); if (valueType != null && String.class == valueType.getType()) { @@ -458,6 +467,10 @@ public class Indexer extends SpelNodeImpl { } } + private static int convertIndexToInt(ExpressionState state, Object index) { + return (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); + } + private static Class getObjectType(Object obj) { return (obj instanceof Class clazz ? clazz : obj.getClass()); } 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 1b50fca17c..52b9e4c020 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 @@ -21,9 +21,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.math.BigDecimal; +import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -44,6 +47,7 @@ import org.springframework.expression.IndexAccessor; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.ReflectiveIndexAccessor; import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.testresources.Person; @@ -735,6 +739,25 @@ class IndexingTests { .havingCause().withMessage("unknown bird: property"); } + @Test // gh-32736 + void readIndexWithCollectionTargetType() { + context.addIndexAccessor(new ColorCollectionIndexAccessor()); + + Expression expression = parser.parseExpression("[0]"); + + // List.of() relies on built-in list support. + assertThat(expression.getValue(context, List.of(Color.RED))).isEqualTo(Color.RED); + + ColorCollection colorCollection = new ColorCollection(); + + // Preconditions for this use case. + assertThat(colorCollection).isInstanceOf(Collection.class); + assertThat(colorCollection).isNotInstanceOf(List.class); + + // ColorCollection relies on custom ColorCollectionIndexAccessor. + assertThat(expression.getValue(context, colorCollection)).isEqualTo(Color.RED); + } + static class BirdNameToColorMappings { public final String property = "enigma"; @@ -755,6 +778,34 @@ class IndexingTests { } } + static class ColorCollection extends AbstractCollection { + + public Color get(int index) { + return switch (index) { + case 0 -> Color.RED; + case 1 -> Color.BLUE; + default -> throw new NoSuchElementException("No color at index " + index); + }; + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + } + + static class ColorCollectionIndexAccessor extends ReflectiveIndexAccessor { + + ColorCollectionIndexAccessor() { + super(ColorCollection.class, int.class, "get"); + } + } + /** * {@link IndexAccessor} that knows how to read and write indexes in a * Jackson {@link ArrayNode}.