diff --git a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java index 693dcffbc1be..5ab301d12f28 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -20,9 +20,12 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.Set; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.util.StringUtils; /** @@ -171,11 +174,69 @@ public MutablePropertyValues addPropertyValue(PropertyValue pv) { setPropertyValueAt(pv, i); return this; } + else if (addNestedPropertyValue(pv, currentPv)) { + return this; + } } this.propertyValueList.add(pv); return this; } + /** + * Add a PropertyValue object, replacing any existing one for the + * corresponding property or getting merged with it (if applicable), + * after drilling down through any chains of beans to locate nested + * properties. + * @param pv PropertyValue object to add + * @param currentPv + * @return true if a matching property was located and merged if applicable, otherwise false + */ + private boolean addNestedPropertyValue(PropertyValue pv, PropertyValue currentPv) { + String pvName = pv.getName(); + // Check if the property has a nested path name and we are at the root of the chain + if (pvName.startsWith(currentPv.getName()) && pvName.contains(".")) { + String[] pvNameComponents = pvName.split("\\."); + // The properties of the current bean in the chain + MutablePropertyValues values = null; + // Counter to track which property we will set in the bean + int k = 0; + // Walk any chain of inner beans until we get to the end + for (int j = 0; j < pvNameComponents.length; j++) { + Object value = currentPv.getValue(); + if (value instanceof BeanDefinitionHolder) { + BeanDefinition bd = ((BeanDefinitionHolder) value).getBeanDefinition(); + values = bd.getPropertyValues(); + if (values != null) { + ListIterator pIterator = + values.getPropertyValueList().listIterator(); + k = 0; + boolean found = false; + while (pIterator.hasNext()) { + PropertyValue v = pIterator.next(); + // Look for the next bean in the chain + if (v.getName().equals(pvNameComponents[j + 1])) { + currentPv = v; + found = true; + break; + } + k++; + } + if (!found) { + throw new FatalBeanException("Bad nested property " + pvName); + } + } + } + } + if (pvName.endsWith(currentPv.getName())) { + // Add the property to the innermost bean properties + pv = mergeIfRequired(pv, currentPv); + values.setPropertyValueAt(pv, k);; + return true; + } + } + return false; + } + /** * Overloaded version of {@code addPropertyValue} that takes * a property name and a property value. @@ -220,10 +281,10 @@ private PropertyValue mergeIfRequired(PropertyValue newPv, PropertyValue current Mergeable mergeable = (Mergeable) value; if (mergeable.isMergeEnabled()) { Object merged = mergeable.merge(currentPv.getValue()); - return new PropertyValue(newPv.getName(), merged); + return new PropertyValue(currentPv.getName(), merged); } } - return newPv; + return new PropertyValue(currentPv.getName(), newPv.getValue()); } /** diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java index 254cd828749f..393fb9af8dc8 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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,9 @@ import static org.hamcrest.Matchers.hasItems; import static org.junit.Assert.assertThat; +import java.util.List; +import java.util.Map; + import org.junit.Test; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -77,6 +80,97 @@ public void defaultMerge() { hasItems("charlie", "delta", "echo", "foxtrot", "golf", "hotel")); } + @Test + @SuppressWarnings("unchecked") + public void defaultMergeNestedPath() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-nested-path-context.xml", this.getClass())); + FooBean foo = bf.getBean("childFoo", FooBean.class); + + // should contain entries added by parent + assertThat("missing key 'kParent'", foo.getMapBean().getMap().containsKey("kParent"), is(true)); + assertThat("missing value 'middleList'", foo.getMapBean().getList().contains("middleList"), is(true)); + assertThat("too many values in list", foo.getMapBean().getList().size() == 1, is(true)); + assertThat("missing value 'lParent'", foo.getMapBean().getListBean().getList().contains("lParent"), is(true)); + + // should also contain entries added by child + assertThat("missing key 'kChild'", foo.getMapBean().getMap().containsKey("kChild"), is(true)); + assertThat("missing value 'lChild'", foo.getMapBean().getListBean().getList().contains("lChild"), is(true)); + assertThat("missing value 'replace'", foo.getMapBean().getListBean().getReplaceList().contains("replace"), is(true)); + assertThat("too many values in replaceList", foo.getMapBean().getListBean().getReplaceList().size() == 1, is(true)); + } + + static class FooBean { + + private MapBean mapBean; + + public MapBean getMapBean() { + return mapBean; + } + + public void setMapBean(MapBean mapBean) { + this.mapBean = mapBean; + } + } + + @SuppressWarnings("rawtypes") + static class MapBean { + + private Map map; + private List list; + private ListBean listBean; + + public void setMap(Map map) { + this.map = map; + } + + public Map getMap() { + return map; + } + + public void setList(List list) { + this.list = list; + } + + public List getList() + { + return list; + } + + public void setListBean(ListBean listBean) { + this.listBean = listBean; + } + + public ListBean getListBean() { + return listBean; + } + } + + @SuppressWarnings("rawtypes") + static class ListBean { + + private List list; + private List replaceList; + + public void setList(List list) { + this.list = list; + } + + public List getList() + { + return list; + } + + public void setReplaceList(List list) { + this.replaceList = list; + } + + public List getReplaceList() { + return replaceList; + } + } + @Test public void defaultAutowireCandidates() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-merge-nested-path-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-merge-nested-path-context.xml new file mode 100644 index 000000000000..71df922df49f --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-merge-nested-path-context.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + middleList + + + + + + + lParent + + + + + original + + + + + + + + + + + + + + + + + lChild + + + + + replace + + + +