diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 17bcbf39b223..8f7f8ea59254 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -649,6 +649,14 @@ else if (value instanceof List list) { growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); value = list.get(index); } + else if (value instanceof Map map) { + Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); + value = map.get(convertedMapKey); + } else if (value instanceof Iterable iterable) { // Apply index to Iterator in case of a Set/Collection/Iterable. int index = Integer.parseInt(key); @@ -676,14 +684,6 @@ else if (value instanceof Iterable iterable) { currIndex + ", accessed using property path '" + propertyName + "'"); } } - else if (value instanceof Map map) { - Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); - // IMPORTANT: Do not pass full property name in here - property editors - // must not kick in for map keys but rather only for map values. - TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); - Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); - value = map.get(convertedMapKey); - } else { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Property referenced in indexed property path '" + propertyName + diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java index f12fd01e9970..ac2387fe7746 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -139,6 +139,7 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isReadableProperty("list")).isTrue(); assertThat(accessor.isReadableProperty("set")).isTrue(); assertThat(accessor.isReadableProperty("map")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans")).isTrue(); assertThat(accessor.isReadableProperty("xxx")).isFalse(); @@ -146,6 +147,7 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isWritableProperty("list")).isTrue(); assertThat(accessor.isWritableProperty("set")).isTrue(); assertThat(accessor.isWritableProperty("map")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap")).isTrue(); assertThat(accessor.isWritableProperty("myTestBeans")).isTrue(); assertThat(accessor.isWritableProperty("xxx")).isFalse(); @@ -161,6 +163,14 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isReadableProperty("map[key4][0].name")).isTrue(); assertThat(accessor.isReadableProperty("map[key4][1]")).isTrue(); assertThat(accessor.isReadableProperty("map[key4][1].name")).isTrue(); + assertThat(accessor.isReadableProperty("map[key999]")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key1]")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key1].name")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key2][0]")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key2][0].name")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key2][1]")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key2][1].name")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key999]")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse(); assertThat(accessor.isReadableProperty("array[key1]")).isFalse(); @@ -177,6 +187,14 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isWritableProperty("map[key4][0].name")).isTrue(); assertThat(accessor.isWritableProperty("map[key4][1]")).isTrue(); assertThat(accessor.isWritableProperty("map[key4][1].name")).isTrue(); + assertThat(accessor.isWritableProperty("map[key999]")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key1]")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key1].name")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key2][0]")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key2][0].name")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key2][1]")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key2][1].name")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key999]")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse(); assertThat(accessor.isWritableProperty("array[key1]")).isFalse(); @@ -1395,6 +1413,9 @@ void getAndSetIndexedProperties() { assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8"); assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8"); assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8"); + assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("nameC"); + assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("nameA"); + assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("nameB"); assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZ"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -1409,6 +1430,9 @@ void getAndSetIndexedProperties() { pvs.add("map[key4][0].name", "nameA"); pvs.add("map[key4][1].name", "nameB"); pvs.add("map[key5[foo]].name", "name10"); + pvs.add("iterableMap[key1].name", "newName1"); + pvs.add("iterableMap[key2][0].name", "newName2A"); + pvs.add("iterableMap[key2][1].name", "newName2B"); pvs.add("myTestBeans[0].name", "nameZZ"); accessor.setPropertyValues(pvs); assertThat(tb0.getName()).isEqualTo("name5"); @@ -1428,6 +1452,9 @@ void getAndSetIndexedProperties() { assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameA"); assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameB"); assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name10"); + assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("newName1"); + assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("newName2A"); + assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("newName2B"); assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZZ"); } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java index 54d32af54535..fe99f9b5bbad 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -49,6 +50,8 @@ public class IndexedTestBean { private SortedMap sortedMap; + private IterableMap iterableMap; + private MyTestBeans myTestBeans; @@ -73,6 +76,9 @@ public void populate() { TestBean tb6 = new TestBean("name6", 0); TestBean tb7 = new TestBean("name7", 0); TestBean tb8 = new TestBean("name8", 0); + TestBean tbA = new TestBean("nameA", 0); + TestBean tbB = new TestBean("nameB", 0); + TestBean tbC = new TestBean("nameC", 0); TestBean tbX = new TestBean("nameX", 0); TestBean tbY = new TestBean("nameY", 0); TestBean tbZ = new TestBean("nameZ", 0); @@ -88,6 +94,12 @@ public void populate() { this.map.put("key2", tb5); this.map.put("key.3", tb5); List list = new ArrayList(); + list.add(tbA); + list.add(tbB); + this.iterableMap = new IterableMap<>(); + this.iterableMap.put("key1", tbC); + this.iterableMap.put("key2", list); + list = new ArrayList(); list.add(tbX); list.add(tbY); this.map.put("key4", list); @@ -152,6 +164,14 @@ public void setSortedMap(SortedMap sortedMap) { this.sortedMap = sortedMap; } + public IterableMap getIterableMap() { + return this.iterableMap; + } + + public void setIterableMap(IterableMap iterableMap) { + this.iterableMap = iterableMap; + } + public MyTestBeans getMyTestBeans() { return myTestBeans; } @@ -161,6 +181,14 @@ public void setMyTestBeans(MyTestBeans myTestBeans) { } + public static class IterableMap extends LinkedHashMap implements Iterable { + + @Override + public Iterator iterator() { + return values().iterator(); + } + } + public static class MyTestBeans implements Iterable { private final Collection testBeans;