Commit c66781a3 authored by Phillip Webb's avatar Phillip Webb

Set using collection copies when possible

Update `Map` and `Collection` binders to create a copy of the existing
collection whenever possible. Prior to this commit the binder would
always mutate the existing value and then call the setter with the
same instance. This could cause issues if the setter expected a
different instance.

Fixes gh-12322
parent 6e2ecb8a
...@@ -65,18 +65,27 @@ class CollectionBinder extends IndexedElementsBinder<Collection<Object>> { ...@@ -65,18 +65,27 @@ class CollectionBinder extends IndexedElementsBinder<Collection<Object>> {
try { try {
existingCollection.clear(); existingCollection.clear();
existingCollection.addAll(additional); existingCollection.addAll(additional);
return existingCollection; return copyIfPossible(existingCollection);
} }
catch (UnsupportedOperationException ex) { catch (UnsupportedOperationException ex) {
return createNewCollection(additional); return createNewCollection(additional);
} }
} }
private Collection<Object> createNewCollection(Collection<Object> additional) { private Collection<Object> copyIfPossible(Collection<Object> collection) {
Collection<Object> merged = CollectionFactory try {
.createCollection(additional.getClass(), additional.size()); return createNewCollection(collection);
merged.addAll(additional); }
return merged; catch (Exception ex) {
return collection;
}
}
private Collection<Object> createNewCollection(Collection<Object> collection) {
Collection<Object> result = CollectionFactory
.createCollection(collection.getClass(), collection.size());
result.addAll(collection);
return result;
} }
} }
...@@ -91,7 +91,19 @@ class MapBinder extends AggregateBinder<Map<Object, Object>> { ...@@ -91,7 +91,19 @@ class MapBinder extends AggregateBinder<Map<Object, Object>> {
return additional; return additional;
} }
existingMap.putAll(additional); existingMap.putAll(additional);
return existingMap; return copyIfPossible(existingMap);
}
private Map<Object, Object> copyIfPossible(Map<Object, Object> map) {
try {
Map<Object, Object> result = CollectionFactory.createMap(map.getClass(),
map.size());
result.putAll(map);
return result;
}
catch (Exception ex) {
return map;
}
} }
private class EntryBinder { private class EntryBinder {
......
...@@ -172,7 +172,6 @@ public class CollectionBinderTests { ...@@ -172,7 +172,6 @@ public class CollectionBinderTests {
List<Integer> result = this.binder List<Integer> result = this.binder
.bind("foo", INTEGER_LIST.withExistingValue(existing)).get(); .bind("foo", INTEGER_LIST.withExistingValue(existing)).get();
assertThat(result).isExactlyInstanceOf(LinkedList.class); assertThat(result).isExactlyInstanceOf(LinkedList.class);
assertThat(result).isSameAs(existing);
assertThat(result).containsExactly(1); assertThat(result).containsExactly(1);
} }
...@@ -309,7 +308,20 @@ public class CollectionBinderTests { ...@@ -309,7 +308,20 @@ public class CollectionBinderTests {
MockConfigurationPropertySource source = new MockConfigurationPropertySource(); MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.items", "a,b,c,c"); source.put("foo.items", "a,b,c,c");
this.sources.add(source); this.sources.add(source);
ExampleCustomBean result = this.binder.bind("foo", ExampleCustomBean.class).get(); ExampleCustomNoDefaultConstructorBean result = this.binder
.bind("foo", ExampleCustomNoDefaultConstructorBean.class).get();
assertThat(result.getItems()).hasSize(4);
assertThat(result.getItems()).containsExactly("a", "b", "c", "c");
}
@Test
public void bindToCollectionWithDefaultConstructor() {
// gh-12322
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.items", "a,b,c,c");
this.sources.add(source);
ExampleCustomWithDefaultConstructorBean result = this.binder
.bind("foo", ExampleCustomWithDefaultConstructorBean.class).get();
assertThat(result.getItems()).hasSize(4); assertThat(result.getItems()).hasSize(4);
assertThat(result.getItems()).containsExactly("a", "b", "c", "c"); assertThat(result.getItems()).containsExactly("a", "b", "c", "c");
} }
...@@ -415,31 +427,46 @@ public class CollectionBinderTests { ...@@ -415,31 +427,46 @@ public class CollectionBinderTests {
} }
} }
public static class ExampleCustomBean { public static class ExampleCustomNoDefaultConstructorBean {
private MyCustomList items = new MyCustomList(Collections.singletonList("foo")); private MyCustomNoDefaultConstructorList items = new MyCustomNoDefaultConstructorList(
Collections.singletonList("foo"));
public MyCustomList getItems() { public MyCustomNoDefaultConstructorList getItems() {
return this.items; return this.items;
} }
public void setItems(MyCustomList items) { public void setItems(MyCustomNoDefaultConstructorList items) {
this.items = items; this.items = items;
} }
} }
public static class MyCustomList extends ArrayList<String> { public static class MyCustomNoDefaultConstructorList extends ArrayList<String> {
private List<String> items; public MyCustomNoDefaultConstructorList(List<String> items) {
addAll(items);
}
public MyCustomList(List<String> items) {
this.items = items;
} }
public List<String> getItems() { public static class ExampleCustomWithDefaultConstructorBean {
private MyCustomWithDefaultConstructorList items = new MyCustomWithDefaultConstructorList();
public MyCustomWithDefaultConstructorList getItems() {
return this.items; return this.items;
} }
public void setItems(MyCustomWithDefaultConstructorList items) {
this.items.clear();
this.items.addAll(items);
}
}
public static class MyCustomWithDefaultConstructorList extends ArrayList<String> {
} }
public static class BeanWithNestedCollection { public static class BeanWithNestedCollection {
......
...@@ -261,7 +261,6 @@ public class MapBinderTests { ...@@ -261,7 +261,6 @@ public class MapBinderTests {
.withExistingValue(existing); .withExistingValue(existing);
Map<String, Integer> result = this.binder.bind("foo", target).get(); Map<String, Integer> result = this.binder.bind("foo", target).get();
assertThat(result).isExactlyInstanceOf(HashMap.class); assertThat(result).isExactlyInstanceOf(HashMap.class);
assertThat(result).isSameAs(existing);
assertThat(result).hasSize(2); assertThat(result).hasSize(2);
assertThat(result).containsEntry("bar", 1); assertThat(result).containsEntry("bar", 1);
assertThat(result).containsEntry("baz", 1001); assertThat(result).containsEntry("baz", 1001);
...@@ -595,6 +594,27 @@ public class MapBinderTests { ...@@ -595,6 +594,27 @@ public class MapBinderTests {
assertThat(map).containsExactly(entry("bar", RuntimeException.class)); assertThat(map).containsExactly(entry("bar", RuntimeException.class));
} }
@Test
public void bindToMapWithNoDefaultConstructor() {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.items.a", "b");
this.sources.add(source);
ExampleCustomNoDefaultConstructorBean result = this.binder
.bind("foo", ExampleCustomNoDefaultConstructorBean.class).get();
assertThat(result.getItems()).containsOnly(entry("foo", "bar"), entry("a", "b"));
}
@Test
public void bindToMapWithDefaultConstructor() {
// gh-12322
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.items.a", "b");
this.sources.add(source);
ExampleCustomWithDefaultConstructorBean result = this.binder
.bind("foo", ExampleCustomWithDefaultConstructorBean.class).get();
assertThat(result.getItems()).containsExactly(entry("a", "b"));
}
private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric, private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric,
ResolvableType valueType) { ResolvableType valueType) {
ResolvableType keyType = ResolvableType.forClass(keyGeneric); ResolvableType keyType = ResolvableType.forClass(keyGeneric);
...@@ -657,4 +677,47 @@ public class MapBinderTests { ...@@ -657,4 +677,47 @@ public class MapBinderTests {
} }
public static class ExampleCustomNoDefaultConstructorBean {
private MyCustomNoDefaultConstructorList items = new MyCustomNoDefaultConstructorList(
Collections.singletonMap("foo", "bar"));
public MyCustomNoDefaultConstructorList getItems() {
return this.items;
}
public void setItems(MyCustomNoDefaultConstructorList items) {
this.items = items;
}
}
public static class MyCustomNoDefaultConstructorList extends HashMap<String, String> {
public MyCustomNoDefaultConstructorList(Map<String, String> items) {
putAll(items);
}
}
public static class ExampleCustomWithDefaultConstructorBean {
private MyCustomWithDefaultConstructorList items = new MyCustomWithDefaultConstructorList();
public MyCustomWithDefaultConstructorList getItems() {
return this.items;
}
public void setItems(MyCustomWithDefaultConstructorList items) {
this.items.clear();
this.items.putAll(items);
}
}
public static class MyCustomWithDefaultConstructorList
extends HashMap<String, String> {
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment