diff --git a/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java index e141fd0e0eb9..746e57a8e98e 100644 --- a/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java @@ -16,6 +16,8 @@ package org.springframework.core.env; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -44,10 +46,43 @@ public MapPropertySource(String name, Map source) { super(name, source); } - + /** + * Returns the value associated with the given property name. + *

+ * First, this method checks for an exact match in the underlying {@code source} map. + * If not found, it attempts to reconstruct a {@link List} from sequentially indexed keys + * (e.g. {@code name[0]}, {@code name[1]}, ...), stopping at the first missing index. + *

+ * Values that implement {@link CharSequence} are converted to plain {@link String} instances. + * + * @param name the property name to resolve + * @return the resolved value, or {@code null} if not found + */ @Override public @Nullable Object getProperty(String name) { - return this.source.get(name); + Object directMatch = this.source.get(name); + + if (directMatch != null) { + return directMatch; + } + + List collectedValues = new ArrayList<>(); + for (int index = 0; ; index++) { + String indexedKey = name + "[" + index + "]"; + if (!this.source.containsKey(indexedKey)) { + break; + } + + Object rawIndexedValue = this.source.get(indexedKey); + + if (rawIndexedValue instanceof CharSequence cs) { + collectedValues.add(cs.toString()); + } else { + collectedValues.add(rawIndexedValue); + } + } + + return collectedValues.isEmpty() ? null : collectedValues; } @Override diff --git a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java index a5c88711fa59..e692fde1d8f2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java @@ -16,11 +16,7 @@ package org.springframework.test.context.support; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; -import java.util.stream.Stream; - +import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; @@ -37,11 +33,16 @@ import org.springframework.mock.env.MockPropertySource; import org.springframework.test.context.TestPropertySource; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.entry; +import java.io.Serial; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -306,6 +307,84 @@ void convertInlinedPropertiesToMapWithNullInlinedProperties() { .withMessageContaining("'inlinedProperties' must not be null"); } + @Test + void returnsListOfStringsFromIndexedKeys() { + Map source = Collections.unmodifiableMap(new HashMap<>() { + @Serial + private static final long serialVersionUID = 5698617178562090885L; + + { + put("first.second[0]", "i"); + put("first.second[1]", "love"); + put("first.second[2]", "spring"); + } + }); + + PropertySource propertySource = new MapPropertySource("test", source); + + Object result = propertySource.getProperty("first.second"); + + assertThat(result) + .isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly("i", "love", "spring"); + } + + @Test + void returnsListOfMixedTypesFromIndexedKeys() { + Map source = Collections.unmodifiableMap(new HashMap<>() { + @Serial + private static final long serialVersionUID = 5698617178562090885L; + + { + put("first.second[0]", "i"); + put("first.second[1]", "love"); + put("first.second[2]", "spring"); + put("first.second[3]", 7); + } + }); + + PropertySource propertySource = new MapPropertySource("test", source); + + Object result = propertySource.getProperty("first.second"); + + assertThat(result) + .isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly("i", "love", "spring", 7); + } + + @Test + void returnsListOfIntegersFromIndexedKeys() { + Map source = Collections.unmodifiableMap(new HashMap<>() { + @Serial + private static final long serialVersionUID = 5698617178562090885L; + + { + put("first.second[0]", 1); + put("first.second[1]", 2); + put("first.second[2]", 3); + } + }); + + PropertySource propertySource = new MapPropertySource("test", source); + + Object result = propertySource.getProperty("first.second"); + + assertThat(result) + .isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(1, 2, 3); + } + + @Test + void returnsNullWhenNoDirectMatchAndNoIndexedKeys() { + Map sourceMap = new HashMap<>(); + PropertySource ps = new MapPropertySource("test", sourceMap); + Object result = ps.getProperty("first.second"); + + assertThat(result).isNull(); + } private static void assertMergedTestPropertySources(Class testClass, String[] expectedLocations, String[] expectedProperties) {