/* * Copyright 2012-2017 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. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.boot.context.properties.bind; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import org.junit.Before; import org.junit.Test; import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.springframework.boot.context.properties.bind.BinderTests.ExampleEnum; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; import org.springframework.core.ResolvableType; import org.springframework.core.env.StandardEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.withSettings; /** * Tests for {@link MapBinder}. * * @author Phillip Webb * @author Madhura Bhave */ public class MapBinderTests { private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable .mapOf(String.class, String.class); private static final Bindable<Map<String, Integer>> STRING_INTEGER_MAP = Bindable .mapOf(String.class, Integer.class); private static final Bindable<Map<Integer, Integer>> INTEGER_INTEGER_MAP = Bindable .mapOf(Integer.class, Integer.class); private static final Bindable<Map<String, Object>> STRING_OBJECT_MAP = Bindable .mapOf(String.class, Object.class); private static final Bindable<Map<String, String[]>> STRING_ARRAY_MAP = Bindable .mapOf(String.class, String[].class); private List<ConfigurationPropertySource> sources = new ArrayList<>(); private Binder binder; @Before public void setup() { this.binder = new Binder(this.sources); } @Test public void bindToMapShouldReturnPopulatedMap() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.bar", "1"); source.put("foo.[baz]", "2"); source.put("foo[BiNg]", "3"); this.sources.add(source); Map<String, String> result = this.binder.bind("foo", STRING_STRING_MAP).get(); assertThat(result).hasSize(3); assertThat(result).containsEntry("bar", "1"); assertThat(result).containsEntry("baz", "2"); assertThat(result).containsEntry("BiNg", "3"); } @Test @SuppressWarnings("unchecked") public void bindToMapWithEmptyPrefix() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.bar", "1"); this.sources.add(source); Map<String, Object> result = this.binder.bind("", STRING_OBJECT_MAP).get(); assertThat((Map<String, Object>) result.get("foo")).containsEntry("bar", "1"); } @Test public void bindToMapShouldConvertMapValue() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.bar", "1"); source.put("foo.[baz]", "2"); source.put("foo[BiNg]", "3"); source.put("faf.bar", "x"); this.sources.add(source); Map<String, Integer> result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); assertThat(result).hasSize(3); assertThat(result).containsEntry("bar", 1); assertThat(result).containsEntry("baz", 2); assertThat(result).containsEntry("BiNg", 3); } @Test public void bindToMapShouldBindToMapValue() throws Exception { ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, ResolvableType.forClass(String.class), STRING_INTEGER_MAP.getType()); MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.bar.baz", "1"); source.put("foo.bar.bin", "2"); source.put("foo.far.baz", "3"); source.put("foo.far.bin", "4"); source.put("faf.far.bin", "x"); this.sources.add(source); Map<String, Map<String, Integer>> result = this.binder .bind("foo", Bindable.<Map<String, Map<String, Integer>>>of(type)).get(); assertThat(result).hasSize(2); assertThat(result.get("bar")).containsEntry("baz", 1).containsEntry("bin", 2); assertThat(result.get("far")).containsEntry("baz", 3).containsEntry("bin", 4); } @Test public void bindToMapShouldBindNestedMapValue() throws Exception { ResolvableType nestedType = ResolvableType.forClassWithGenerics(Map.class, ResolvableType.forClass(String.class), STRING_INTEGER_MAP.getType()); ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, ResolvableType.forClass(String.class), nestedType); MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.nested.bar.baz", "1"); source.put("foo.nested.bar.bin", "2"); source.put("foo.nested.far.baz", "3"); source.put("foo.nested.far.bin", "4"); source.put("faf.nested.far.bin", "x"); this.sources.add(source); Bindable<Map<String, Map<String, Map<String, Integer>>>> target = Bindable .of(type); Map<String, Map<String, Map<String, Integer>>> result = this.binder .bind("foo", target).get(); Map<String, Map<String, Integer>> nested = result.get("nested"); assertThat(nested).hasSize(2); assertThat(nested.get("bar")).containsEntry("baz", 1).containsEntry("bin", 2); assertThat(nested.get("far")).containsEntry("baz", 3).containsEntry("bin", 4); } @Test @SuppressWarnings("unchecked") public void bindToMapWhenMapValueIsObjectShouldBindNestedMapValue() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.nested.bar.baz", "1"); source.put("foo.nested.bar.bin", "2"); source.put("foo.nested.far.baz", "3"); source.put("foo.nested.far.bin", "4"); source.put("faf.nested.far.bin", "x"); this.sources.add(source); Map<String, Object> result = this.binder .bind("foo", Bindable.mapOf(String.class, Object.class)).get(); Map<String, Object> nested = (Map<String, Object>) result.get("nested"); assertThat(nested).hasSize(2); Map<String, Object> bar = (Map<String, Object>) nested.get("bar"); assertThat(bar).containsEntry("baz", "1").containsEntry("bin", "2"); Map<String, Object> far = (Map<String, Object>) nested.get("far"); assertThat(far).containsEntry("baz", "3").containsEntry("bin", "4"); } @Test public void bindToMapWhenMapValueIsObjectAndNoRootShouldBindNestedMapValue() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("commit.id", "abcdefg"); source.put("branch", "master"); source.put("foo", "bar"); this.sources.add(source); Map<String, Object> result = this.binder .bind("", Bindable.mapOf(String.class, Object.class)).get(); assertThat(result.get("commit")) .isEqualTo(Collections.singletonMap("id", "abcdefg")); assertThat(result.get("branch")).isEqualTo("master"); assertThat(result.get("foo")).isEqualTo("bar"); } @Test public void bindToMapWhenEmptyRootNameShouldBindMap() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("bar.baz", "1"); source.put("bar.bin", "2"); this.sources.add(source); Map<String, Integer> result = this.binder.bind("", STRING_INTEGER_MAP).get(); assertThat(result).hasSize(2); assertThat(result).containsEntry("bar.baz", 1).containsEntry("bar.bin", 2); } @Test public void bindToMapWhenMultipleCandidateShouldBindFirst() throws Exception { MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); source1.put("foo.bar", "1"); source1.put("foo.baz", "2"); this.sources.add(source1); MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); source2.put("foo.baz", "3"); source2.put("foo.bin", "4"); this.sources.add(source2); Map<String, Integer> result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); assertThat(result).hasSize(3); assertThat(result).containsEntry("bar", 1); assertThat(result).containsEntry("baz", 2); assertThat(result).containsEntry("bin", 4); } @Test public void bindToMapWhenMultipleInSameSourceCandidateShouldBindFirst() throws Exception { Map<String, Object> map = new HashMap<>(); map.put("foo.bar", "1"); map.put("foo.b-az", "2"); map.put("foo.ba-z", "3"); map.put("foo.bin", "4"); MapConfigurationPropertySource propertySource = new MapConfigurationPropertySource( map); this.sources.add(propertySource); Map<String, Integer> result = this.binder.bind("foo", STRING_INTEGER_MAP).get(); assertThat(result).hasSize(4); assertThat(result).containsEntry("bar", 1); assertThat(result).containsEntry("b-az", 2); assertThat(result).containsEntry("ba-z", 3); assertThat(result).containsEntry("bin", 4); } @Test public void bindToMapWhenHasExistingMapShouldReplaceOnlyNewContents() throws Exception { this.sources.add(new MockConfigurationPropertySource("foo.bar", "1")); Map<String, Integer> existing = new HashMap<>(); existing.put("bar", 1000); existing.put("baz", 1001); Bindable<Map<String, Integer>> target = STRING_INTEGER_MAP .withExistingValue(existing); Map<String, Integer> result = this.binder.bind("foo", target).get(); assertThat(result).isExactlyInstanceOf(HashMap.class); assertThat(result).isSameAs(existing); assertThat(result).hasSize(2); assertThat(result).containsEntry("bar", 1); assertThat(result).containsEntry("baz", 1001); } @Test public void bindToMapShouldRespectMapType() throws Exception { this.sources.add(new MockConfigurationPropertySource("foo.bar", "1")); ResolvableType type = ResolvableType.forClassWithGenerics(HashMap.class, String.class, Integer.class); Object defaultMap = this.binder.bind("foo", STRING_INTEGER_MAP).get(); Object customMap = this.binder.bind("foo", Bindable.of(type)).get(); assertThat(customMap).isExactlyInstanceOf(HashMap.class) .isNotInstanceOf(defaultMap.getClass()); } @Test public void bindToMapWhenNoValueShouldReturnUnbound() throws Exception { this.sources.add(new MockConfigurationPropertySource("faf.bar", "1")); BindResult<Map<String, Integer>> result = this.binder.bind("foo", STRING_INTEGER_MAP); assertThat(result.isBound()).isFalse(); } @Test public void bindToMapShouldConvertKey() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo[0]", "1"); source.put("foo[1]", "2"); source.put("foo[9]", "3"); this.sources.add(source); Map<Integer, Integer> result = this.binder.bind("foo", INTEGER_INTEGER_MAP).get(); assertThat(result).hasSize(3); assertThat(result).containsEntry(0, 1); assertThat(result).containsEntry(1, 2); assertThat(result).containsEntry(9, 3); } @Test public void bindToMapShouldBeGreedyForStrings() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.aaa.bbb.ccc", "b"); source.put("foo.bbb.ccc.ddd", "a"); source.put("foo.ccc.ddd.eee", "r"); this.sources.add(source); Map<String, String> result = this.binder.bind("foo", STRING_STRING_MAP).get(); assertThat(result).hasSize(3); assertThat(result).containsEntry("aaa.bbb.ccc", "b"); assertThat(result).containsEntry("bbb.ccc.ddd", "a"); assertThat(result).containsEntry("ccc.ddd.eee", "r"); } @Test public void bindToMapShouldBeGreedyForScalars() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.aaa.bbb.ccc", "foo-bar"); source.put("foo.bbb.ccc.ddd", "BAR_BAZ"); source.put("foo.ccc.ddd.eee", "bazboo"); this.sources.add(source); Map<String, ExampleEnum> result = this.binder .bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get(); assertThat(result).hasSize(3); assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.FOO_BAR); assertThat(result).containsEntry("bbb.ccc.ddd", ExampleEnum.BAR_BAZ); assertThat(result).containsEntry("ccc.ddd.eee", ExampleEnum.BAZ_BOO); } @Test public void bindToMapWithPlaceholdersShouldBeGreedyForScalars() throws Exception { StandardEnvironment environment = new StandardEnvironment(); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, "foo=boo"); MockConfigurationPropertySource source = new MockConfigurationPropertySource( "foo.aaa.bbb.ccc", "baz-${foo}"); this.sources.add(source); this.binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(environment)); Map<String, ExampleEnum> result = this.binder .bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get(); assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.BAZ_BOO); } @Test public void bindToMapWithNoPropertiesShouldReturnUnbound() throws Exception { this.binder = new Binder(this.sources); BindResult<Map<String, ExampleEnum>> result = this.binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)); assertThat(result.isBound()).isFalse(); } @Test public void bindToMapShouldTriggerOnSuccess() throws Exception { this.sources.add(new MockConfigurationPropertySource("foo.bar", "1", "line1")); BindHandler handler = mock(BindHandler.class, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); Bindable<Map<String, Integer>> target = STRING_INTEGER_MAP; this.binder.bind("foo", target, handler); InOrder inOrder = inOrder(handler); inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo.bar")), eq(Bindable.of(Integer.class)), any(), eq(1)); inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), eq(target), any(), isA(Map.class)); } @Test public void bindToMapStringArrayShouldTriggerOnSuccess() throws Exception { this.sources .add(new MockConfigurationPropertySource("foo.bar", "a,b,c", "line1")); BindHandler handler = mock(BindHandler.class, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); Bindable<Map<String, String[]>> target = STRING_ARRAY_MAP; this.binder.bind("foo", target, handler); InOrder inOrder = inOrder(handler); ArgumentCaptor<String[]> array = ArgumentCaptor.forClass(String[].class); inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo.bar")), eq(Bindable.of(String[].class)), any(), array.capture()); assertThat(array.getValue()).containsExactly("a", "b", "c"); inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), eq(target), any(), isA(Map.class)); } @Test public void bindToPropertiesShouldBeEquivalentToMapOfStringString() throws Exception { this.sources .add(new MockConfigurationPropertySource("foo.bar.baz", "1", "line1")); BindHandler handler = mock(BindHandler.class, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); Bindable<Properties> target = Bindable.of(Properties.class); this.binder.bind("foo", target, handler); InOrder inOrder = inOrder(handler); inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo.bar.baz")), eq(Bindable.of(String.class)), any(), eq("1")); inOrder.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), eq(target), any(), isA(Properties.class)); } }