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 c52fc482bca..c513d8f6f41 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 @@ -244,14 +244,7 @@ public class Indexer extends SpelNodeImpl { } } - // Try to treat the index value as a property of the context object. - TypeDescriptor valueType = indexValue.getTypeDescriptor(); - if (valueType != null && String.class == valueType.getType()) { - this.indexedType = IndexedType.OBJECT; - return new PropertyAccessorValueRef( - target, (String) index, state.getEvaluationContext(), targetDescriptor); - } - + // Check for a custom IndexAccessor. EvaluationContext evalContext = state.getEvaluationContext(); List accessorsToTry = getIndexAccessorsToTry(target, evalContext.getIndexAccessors()); if (accessMode.supportsReads) { @@ -285,6 +278,14 @@ public class Indexer extends SpelNodeImpl { } } + // 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()) { + this.indexedType = IndexedType.OBJECT; + return new PropertyAccessorValueRef( + target, (String) index, state.getEvaluationContext(), targetDescriptor); + } + throw new SpelEvaluationException( getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetDescriptor); } 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 dad3e73d5ae..74460e14090 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 @@ -711,6 +711,72 @@ class IndexingTests { assertThat(expr.getValue(context, arrayNode)).isSameAs(node1); } + @Test // gh-32706 + void readIndexWithStringIndexType() { + BirdNameToColorMappings birdNameMappings = new BirdNameToColorMappings(); + + // Without a registered BirdNameToColorMappingsIndexAccessor, we should + // be able to index into an object via a property name. + Expression propertyExpression = parser.parseExpression("['property']"); + assertThat(propertyExpression.getValue(context, birdNameMappings)).isEqualTo("enigma"); + + context.addIndexAccessor(new BirdNameToColorMappingsIndexAccessor()); + + Expression expression = parser.parseExpression("['cardinal']"); + assertThat(expression.getValue(context, birdNameMappings)).isEqualTo(Color.RED); + + // With a registered BirdNameToColorMappingsIndexAccessor, an attempt + // to index into an object via a property name should fail. + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> propertyExpression.getValue(context, birdNameMappings)) + .withMessageEndingWith("A problem occurred while attempting to read index '%s' in '%s'", + "property", BirdNameToColorMappings.class.getName()) + .havingCause().withMessage("unknown bird color: property"); + } + + static class BirdNameToColorMappings { + + public final String property = "enigma"; + + public Color get(String name) { + return switch (name) { + case "cardinal" -> Color.RED; + case "blue jay" -> Color.BLUE; + default -> throw new RuntimeException("unknown bird color: " + name); + }; + } + } + + static class BirdNameToColorMappingsIndexAccessor implements IndexAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] { BirdNameToColorMappings.class }; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, Object index) { + return (target instanceof BirdNameToColorMappings && index instanceof String); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, Object index) { + BirdNameToColorMappings mappings = (BirdNameToColorMappings) target; + String name = (String) index; + return new TypedValue(mappings.get(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, Object index) { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) { + throw new UnsupportedOperationException(); + } + } + /** * {@link IndexAccessor} that knows how to read and write indexes in a * Jackson {@link ArrayNode}.