diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java index 9a4b1bad6a9..d46379be33a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java @@ -17,6 +17,7 @@ package org.springframework.boot.context.properties.bind; import java.util.Collection; +import java.util.EnumMap; import java.util.Map; import java.util.Properties; import java.util.function.Supplier; @@ -65,8 +66,7 @@ class MapBinder extends AggregateBinder> { } } } - Map map = CollectionFactory - .createMap((target.getValue() != null) ? Map.class : target.getType().resolve(Object.class), 0); + Map map = createMap(target); for (ConfigurationPropertySource source : getContext().getSources()) { if (!ConfigurationPropertyName.EMPTY.equals(name)) { source = source.filter(name::isAncestorOf); @@ -76,6 +76,15 @@ class MapBinder extends AggregateBinder> { return map.isEmpty() ? null : map; } + private Map createMap(Bindable target) { + Class mapType = (target.getValue() != null) ? Map.class : target.getType().resolve(Object.class); + if (EnumMap.class.isAssignableFrom(mapType)) { + Class keyType = target.getType().asMap().getGeneric(0).resolve(); + return CollectionFactory.createMap(mapType, keyType, 0); + } + return CollectionFactory.createMap(mapType, 0); + } + private boolean hasDescendants(ConfigurationPropertyName name) { for (ConfigurationPropertySource source : getContext().getSources()) { if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index aecac3e612d..9972390b21d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -25,6 +25,7 @@ import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -156,6 +157,10 @@ class ValueObjectBinder implements DataObjectBinder { if (Collection.class.isAssignableFrom(resolved)) { return (T) CollectionFactory.createCollection(resolved, 0); } + if (EnumMap.class.isAssignableFrom(resolved)) { + Class keyType = type.asMap().getGeneric(0).resolve(); + return (T) CollectionFactory.createMap(resolved, keyType, 0); + } if (Map.class.isAssignableFrom(resolved)) { return (T) CollectionFactory.createMap(resolved, 0); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java index 0a23259e096..b875d304a7a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -20,6 +20,7 @@ import java.net.InetAddress; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -79,6 +80,9 @@ class MapBinderTests { private static final Bindable> STRING_ARRAY_MAP = Bindable.mapOf(String.class, String[].class); + private static final Bindable> EXAMPLE_ENUM_STRING_ENUM_MAP = Bindable + .of(ResolvableType.forClassWithGenerics(EnumMap.class, ExampleEnum.class, String.class)); + private final List sources = new ArrayList<>(); private final Binder binder = new Binder(this.sources); @@ -637,6 +641,17 @@ class MapBinderTests { assertThat(result.getCustomMap().getSource()).isEqualTo("value"); } + @Test + void bindToEnumMapShouldBind() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("props.foo-bar", "value"); + this.sources.add(source); + Binder binder = new Binder(this.sources, null, null, null); + EnumMap result = binder.bind("props", EXAMPLE_ENUM_STRING_ENUM_MAP).get(); + assertThat(result).hasSize(1).containsEntry(ExampleEnum.FOO_BAR, "value"); + + } + private Bindable> getMapBindable(Class keyGeneric, ResolvableType valueType) { ResolvableType keyType = ResolvableType.forClass(keyGeneric); return Bindable.of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType)); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index ac95a9d1d61..0d39e4316be 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -306,6 +307,13 @@ class ValueObjectBinderTests { assertThat(bound.getMapValue()).isEmpty(); } + @Test + void bindWhenEnumMapParametersWithEmptyDefaultValueShouldReturnEmptyInstance() { + NestedConstructorBeanWithEmptyDefaultValueForEnumMapTypes bound = this.binder.bindOrCreate("foo", + Bindable.of(NestedConstructorBeanWithEmptyDefaultValueForEnumMapTypes.class)); + assertThat(bound.getMapValue()).isEmpty(); + } + @Test void bindWhenArrayParameterWithEmptyDefaultValueShouldReturnEmptyInstance() { NestedConstructorBeanWithEmptyDefaultValueForArrayTypes bound = this.binder.bindOrCreate("foo", @@ -781,6 +789,20 @@ class ValueObjectBinderTests { } + static class NestedConstructorBeanWithEmptyDefaultValueForEnumMapTypes { + + private final EnumMap mapValue; + + NestedConstructorBeanWithEmptyDefaultValueForEnumMapTypes(@DefaultValue EnumMap mapValue) { + this.mapValue = mapValue; + } + + EnumMap getMapValue() { + return this.mapValue; + } + + } + static class NestedConstructorBeanWithEmptyDefaultValueForArrayTypes { private final String[] arrayValue;