Return-Path: Delivered-To: apmail-jakarta-commons-dev-archive@apache.org Received: (qmail 57735 invoked from network); 4 Feb 2003 07:28:18 -0000 Received: from exchange.sun.com (192.18.33.10) by daedalus.apache.org with SMTP; 4 Feb 2003 07:28:18 -0000 Received: (qmail 18908 invoked by uid 97); 4 Feb 2003 07:29:57 -0000 Delivered-To: qmlist-jakarta-archive-commons-dev@nagoya.betaversion.org Received: (qmail 18901 invoked from network); 4 Feb 2003 07:29:56 -0000 Received: from daedalus.apache.org (HELO apache.org) (208.185.179.12) by nagoya.betaversion.org with SMTP; 4 Feb 2003 07:29:56 -0000 Received: (qmail 57556 invoked by uid 500); 4 Feb 2003 07:28:16 -0000 Mailing-List: contact commons-dev-help@jakarta.apache.org; run by ezmlm Precedence: bulk List-Unsubscribe: List-Subscribe: List-Help: List-Post: List-Id: "Jakarta Commons Developers List" Reply-To: "Jakarta Commons Developers List" Delivered-To: mailing list commons-dev@jakarta.apache.org Received: (qmail 57542 invoked by uid 500); 4 Feb 2003 07:28:16 -0000 Received: (qmail 57537 invoked from network); 4 Feb 2003 07:28:15 -0000 Received: from icarus.apache.org (208.185.179.13) by daedalus.apache.org with SMTP; 4 Feb 2003 07:28:15 -0000 Received: (qmail 13661 invoked by uid 1059); 4 Feb 2003 07:28:14 -0000 Date: 4 Feb 2003 07:28:14 -0000 Message-ID: <20030204072814.13660.qmail@icarus.apache.org> From: craigmcc@apache.org To: jakarta-commons-cvs@apache.org Subject: cvs commit: jakarta-commons/beanutils/src/test/org/apache/commons/beanutils BeanUtilsTestCase.java DynaBeanUtilsTestCase.java X-Spam-Rating: daedalus.apache.org 1.6.2 0/1000/N X-Spam-Rating: daedalus.apache.org 1.6.2 0/1000/N craigmcc 2003/02/03 23:28:14 Modified: beanutils/src/java/org/apache/commons/beanutils BeanUtils.java beanutils/src/test/org/apache/commons/beanutils BeanUtilsTestCase.java DynaBeanUtilsTestCase.java Log: Enhance BeanUtils.copyProperty() to deal with about 80% of the use cases for copying indexed, mapped, and nested properties with type conversions. The remaining restrictions are documented in the Javadocs for this method. This is a partial response to Bugzilla #16525, which documents some restrictions in the functionality of copyProperty() that leads people to try setProperty() instead -- which they should not do. It is much more conservative than the proposed patch, which (as the reporter acknowledges) is more appropriate in a minor update (1.7) versus a bugfix patch (1.6.1) which is currently contemplated. PR: Bugzilla #16525 Submitted by: Tim Vernum Revision Changes Path 1.36 +147 -62 jakarta-commons/beanutils/src/java/org/apache/commons/beanutils/BeanUtils.java Index: BeanUtils.java =================================================================== RCS file: /home/cvs/jakarta-commons/beanutils/src/java/org/apache/commons/beanutils/BeanUtils.java,v retrieving revision 1.35 retrieving revision 1.36 diff -u -r1.35 -r1.36 --- BeanUtils.java 15 Jan 2003 21:59:38 -0000 1.35 +++ BeanUtils.java 4 Feb 2003 07:28:14 -0000 1.36 @@ -153,7 +153,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static Object cloneBean(Object bean) throws IllegalAccessException, InstantiationException, @@ -275,18 +275,28 @@ /** *

Copy the specified property value to the specified destination bean, * performing any type conversion that is required. If the specified - * bean does not have a property of the specified name, return without + * bean does not have a property of the specified name, or the property + * is read only on the destination bean, return without * doing anything. If you have custom destination property types, register * {@link Converter}s for them by calling the register() * method of {@link ConvertUtils}.

* - *

FIXME - Indexed and mapped properties that do not - * have getter and setter methods for the underlying array or Map are not - * copied by this method.

+ *

IMPLEMENTATION RESTRICTIONS:

+ *
    + *
  • Does not support destination properties that are indexed, + * but only an indexed setter (as opposed to an array setter) + * is available.
  • + *
  • Does not support destination properties that are mapped, + * but only a keyed setter (as opposed to a Map setter) + * is available.
  • + *
  • The desired property type of a mapped setter cannot be + * determined (since Maps support any data type), so no conversion + * will be performed.
  • + *
* * @param bean Bean on which setting is to be performed - * @param name Simple property name of the property to be set - * @param value Property value to be set + * @param name Property name (can be nested/indexed/mapped/combo) + * @param value Value to be set * * @exception IllegalAccessException if the caller does not have * access to the property accessor method @@ -296,6 +306,7 @@ public static void copyProperty(Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException { + // Trace logging (if enabled) if (log.isTraceEnabled()) { StringBuffer sb = new StringBuffer(" copyProperty("); sb.append(bean); @@ -323,55 +334,123 @@ log.trace(sb.toString()); } - if (bean instanceof DynaBean) { - DynaProperty propDescriptor = - ((DynaBean) bean).getDynaClass().getDynaProperty(name); - if (propDescriptor != null) { - Converter converter = - ConvertUtils.lookup(propDescriptor.getType()); - if (converter != null) { - value = converter.convert(propDescriptor.getType(), value); - } - try { - PropertyUtils.setSimpleProperty(bean, name, value); - } catch (NoSuchMethodException e) { - log.error("-->Should not have happened", e); - ; // Silently ignored + // Resolve any nested expression to get the actual target bean + Object target = bean; + int delim = name.lastIndexOf(PropertyUtils.NESTED_DELIM); + if (delim >= 0) { + try { + target = + PropertyUtils.getProperty(bean, name.substring(0, delim)); + } catch (NoSuchMethodException e) { + return; // Skip this property setter + } + name = name.substring(delim + 1); + if (log.isTraceEnabled()) { + log.trace(" Target bean = " + target); + log.trace(" Target name = " + name); + } + } + + // Declare local variables we will require + String propName = null; // Simple name of target property + Class type = null; // Java type of target property + int index = -1; // Indexed subscript value (if any) + String key = null; // Mapped key value (if any) + + // Calculate the target property name, index, and key values + propName = name; + int i = propName.indexOf(PropertyUtils.INDEXED_DELIM); + if (i >= 0) { + int k = propName.indexOf(PropertyUtils.INDEXED_DELIM2); + try { + index = + Integer.parseInt(propName.substring(i + 1, k)); + } catch (NumberFormatException e) { + ; + } + propName = propName.substring(0, i); + } + int j = propName.indexOf(PropertyUtils.MAPPED_DELIM); + if (j >= 0) { + int k = propName.indexOf(PropertyUtils.MAPPED_DELIM2); + try { + key = propName.substring(j + 1, k); + } catch (IndexOutOfBoundsException e) { + ; + } + propName = propName.substring(0, j); + } + + // Calculate the target property type + if (target instanceof DynaBean) { + DynaClass dynaClass = ((DynaBean) target).getDynaClass(); + DynaProperty dynaProperty = dynaClass.getDynaProperty(propName); + if (dynaProperty == null) { + return; // Skip this property setter + } + type = dynaProperty.getType(); + } else { + PropertyDescriptor descriptor = null; + try { + descriptor = + PropertyUtils.getPropertyDescriptor(target, name); + if (descriptor == null) { + return; // Skip this property setter } - } else { + } catch (NoSuchMethodException e) { + return; // Skip this property setter + } + type = descriptor.getPropertyType(); + if (type == null) { + // Most likely an indexed setter on a POJB only if (log.isTraceEnabled()) { - log.trace("-->No setter on 'to' DynaBean, skipping"); + log.trace(" target type for property '" + + propName + "' is null, so skipping ths setter"); } + return; + } + } + if (log.isTraceEnabled()) { + log.trace(" target propName=" + propName + ", type=" + + type + ", index=" + index + ", key=" + key); + } + + // Convert the specified value to the required type and store it + if (index >= 0) { // Destination must be indexed + Converter converter = ConvertUtils.lookup(type.getComponentType()); + if (converter != null) { + log.trace(" USING CONVERTER " + converter); + value = converter.convert(type, value); } - } else /* if (!(bean instanceof DynaBean)) */ { - PropertyDescriptor propDescriptor = null; try { - propDescriptor = - PropertyUtils.getPropertyDescriptor(bean, name); + PropertyUtils.setIndexedProperty(target, propName, + index, value); } catch (NoSuchMethodException e) { - propDescriptor = null; + throw new InvocationTargetException + (e, "Cannot set " + propName); } - if ((propDescriptor != null) && - (propDescriptor.getWriteMethod() == null)) { - propDescriptor = null; - } - if (propDescriptor != null) { - Converter converter = - ConvertUtils.lookup(propDescriptor.getPropertyType()); - if (converter != null) { - value = converter.convert - (propDescriptor.getPropertyType(), value); - } - try { - PropertyUtils.setSimpleProperty(bean, name, value); - } catch (NoSuchMethodException e) { - log.error("-->Should not have happened", e); - ; // Silently ignored - } - } else { - if (log.isTraceEnabled()) { - log.trace("-->No setter on JavaBean, skipping"); - } + } else if (key != null) { // Destination must be mapped + // Maps do not know what the preferred data type is, + // so perform no conversions at all + // FIXME - should we create or support a TypedMap? + try { + PropertyUtils.setMappedProperty(target, propName, + key, value); + } catch (NoSuchMethodException e) { + throw new InvocationTargetException + (e, "Cannot set " + propName); + } + } else { // Destination must be simple + Converter converter = ConvertUtils.lookup(type); + if (converter != null) { + log.trace(" USING CONVERTER " + converter); + value = converter.convert(type, value); + } + try { + PropertyUtils.setSimpleProperty(target, propName, value); + } catch (NoSuchMethodException e) { + throw new InvocationTargetException + (e, "Cannot set " + propName); } } @@ -392,7 +471,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static Map describe(Object bean) throws IllegalAccessException, InvocationTargetException, @@ -441,7 +520,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static String[] getArrayProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, @@ -499,7 +578,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static String getIndexedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, @@ -525,7 +604,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static String getIndexedProperty(Object bean, String name, int index) @@ -554,7 +633,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static String getMappedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, @@ -580,7 +659,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static String getMappedProperty(Object bean, String name, String key) @@ -607,7 +686,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static String getNestedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, @@ -632,7 +711,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static String getProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, @@ -655,7 +734,7 @@ * @exception InvocationTargetException if the property accessor method * throws an exception * @exception NoSuchMethodException if an accessor method for this - * propety cannot be found + * property cannot be found */ public static String getSimpleProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, @@ -747,6 +826,12 @@ * to meet the needs of populate(), and is probably not what * you want for general property copying with type conversion. For that * purpose, check out the copyProperty() method instead.

+ * + *

WARNING - PLEASE do not modify the behavior of this + * method without consulting with the Struts developer community. There + * are some subtleties to its functionality that are not documented in the + * Javadoc description above, yet are vital to the way that Struts utilizes + * this method.

* * @param bean Bean on which setting is to be performed * @param name Property name (can be nested/indexed/mapped/combo) 1.20 +125 -4 jakarta-commons/beanutils/src/test/org/apache/commons/beanutils/BeanUtilsTestCase.java Index: BeanUtilsTestCase.java =================================================================== RCS file: /home/cvs/jakarta-commons/beanutils/src/test/org/apache/commons/beanutils/BeanUtilsTestCase.java,v retrieving revision 1.19 retrieving revision 1.20 diff -u -r1.19 -r1.20 --- BeanUtilsTestCase.java 1 Feb 2003 08:14:33 -0000 1.19 +++ BeanUtilsTestCase.java 4 Feb 2003 07:28:14 -0000 1.20 @@ -64,6 +64,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import junit.framework.Test; @@ -1112,6 +1113,126 @@ BeanUtils.copyProperty(bean, "shortProperty", new Short((short) 123)); assertEquals((short) 123, bean.getShortProperty()); + } + + + /** + * Test copying a property using a nested indexed array expression, + * with and without conversions. + */ + public void testCopyPropertyNestedIndexedArray() throws Exception { + + int origArray[] = { 0, 10, 20, 30, 40 }; + int intArray[] = { 0, 0, 0 }; + bean.getNested().setIntArray(intArray); + int intChanged[] = { 0, 0, 0 }; + + // No conversion required + BeanUtils.copyProperty(bean, "nested.intArray[1]", new Integer(1)); + checkIntArray(bean.getIntArray(), origArray); + intChanged[1] = 1; + checkIntArray(bean.getNested().getIntArray(), intChanged); + + // Widening conversion required + BeanUtils.copyProperty(bean, "nested.intArray[1]", new Byte((byte) 2)); + checkIntArray(bean.getIntArray(), origArray); + intChanged[1] = 2; + checkIntArray(bean.getNested().getIntArray(), intChanged); + + // Narrowing conversion required + BeanUtils.copyProperty(bean, "nested.intArray[1]", new Long((long) 3)); + checkIntArray(bean.getIntArray(), origArray); + intChanged[1] = 3; + checkIntArray(bean.getNested().getIntArray(), intChanged); + + // String conversion required + BeanUtils.copyProperty(bean, "nested.intArray[1]", "4"); + checkIntArray(bean.getIntArray(), origArray); + intChanged[1] = 4; + checkIntArray(bean.getNested().getIntArray(), intChanged); + + } + + + /** + * Test copying a property using a nested mapped map property. + */ + public void testCopyPropertyNestedMappedMap() throws Exception { + + Map origMap = new HashMap(); + origMap.put("First Key", "First Value"); + origMap.put("Second Key", "Second Value"); + Map changedMap = new HashMap(); + changedMap.put("First Key", "First Value"); + changedMap.put("Second Key", "Second Value"); + + // No conversion required + BeanUtils.copyProperty(bean, "nested.mapProperty(Second Key)", + "New Second Value"); + checkMap(bean.getMapProperty(), origMap); + changedMap.put("Second Key", "New Second Value"); + checkMap(bean.getNested().getMapProperty(), changedMap); + + } + + + /** + * Test copying a property using a nested simple expression, with and + * without conversions. + */ + public void testCopyPropertyNestedSimple() throws Exception { + + bean.setIntProperty(0); + bean.getNested().setIntProperty(0); + + // No conversion required + BeanUtils.copyProperty(bean, "nested.intProperty", new Integer(1)); + assertNotNull(bean.getNested()); + assertEquals(0, bean.getIntProperty()); + assertEquals(1, bean.getNested().getIntProperty()); + + // Widening conversion required + BeanUtils.copyProperty(bean, "nested.intProperty", new Byte((byte) 2)); + assertNotNull(bean.getNested()); + assertEquals(0, bean.getIntProperty()); + assertEquals(2, bean.getNested().getIntProperty()); + + // Narrowing conversion required + BeanUtils.copyProperty(bean, "nested.intProperty", new Long((long) 3)); + assertNotNull(bean.getNested()); + assertEquals(0, bean.getIntProperty()); + assertEquals(3, bean.getNested().getIntProperty()); + + // String conversion required + BeanUtils.copyProperty(bean, "nested.intProperty", "4"); + assertNotNull(bean.getNested()); + assertEquals(0, bean.getIntProperty()); + assertEquals(4, bean.getNested().getIntProperty()); + + } + + + // Ensure that the actual int[] matches the expected int[] + protected void checkIntArray(int actual[], int expected[]) { + assertNotNull("actual array not null", actual); + assertEquals("actual array length", expected.length, actual.length); + for (int i = 0; i < actual.length; i++) { + assertEquals("actual array value[" + i + "]", + expected[i], actual[i]); + } + } + + + // Ensure that the actual Map matches the expected Map + protected void checkMap(Map actual, Map expected) { + assertNotNull("actual map not null", actual); + assertEquals("actual map size", expected.size(), actual.size()); + Iterator keys = expected.keySet().iterator(); + while (keys.hasNext()) { + Object key = keys.next(); + assertEquals("actual map value(" + key + ")", + expected.get(key), actual.get(key)); + } } 1.17 +131 -4 jakarta-commons/beanutils/src/test/org/apache/commons/beanutils/DynaBeanUtilsTestCase.java Index: DynaBeanUtilsTestCase.java =================================================================== RCS file: /home/cvs/jakarta-commons/beanutils/src/test/org/apache/commons/beanutils/DynaBeanUtilsTestCase.java,v retrieving revision 1.16 retrieving revision 1.17 diff -u -r1.16 -r1.17 --- DynaBeanUtilsTestCase.java 1 Feb 2003 08:14:33 -0000 1.16 +++ DynaBeanUtilsTestCase.java 4 Feb 2003 07:28:14 -0000 1.17 @@ -65,6 +65,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -113,6 +114,7 @@ "intProperty", "listIndexed", "longProperty", + "mapProperty", "mappedProperty", "mappedIntProperty", "nested", @@ -173,6 +175,10 @@ listIndexed.add("String 4"); bean.set("listIndexed", listIndexed); bean.set("longProperty", new Long((long) 321)); + HashMap mapProperty = new HashMap(); + mapProperty.put("First Key", "First Value"); + mapProperty.put("Second Key", "Second Value"); + bean.set("mapProperty", mapProperty); HashMap mappedProperty = new HashMap(); mappedProperty.put("First Key", "First Value"); mappedProperty.put("Second Key", "Second Value"); @@ -1037,9 +1043,129 @@ } + /** + * Test copying a property using a nested indexed array expression, + * with and without conversions. + */ + public void testCopyPropertyNestedIndexedArray() throws Exception { + + int origArray[] = { 0, 10, 20, 30, 40}; + int intArray[] = { 0, 0, 0 }; + ((TestBean) bean.get("nested")).setIntArray(intArray); + int intChanged[] = { 0, 0, 0 }; + + // No conversion required + BeanUtils.copyProperty(bean, "nested.intArray[1]", new Integer(1)); + checkIntArray((int[]) bean.get("intArray"), origArray); + intChanged[1] = 1; + checkIntArray(((TestBean) bean.get("nested")).getIntArray(), + intChanged); + + // Widening conversion required + BeanUtils.copyProperty(bean, "nested.intArray[1]", new Byte((byte) 2)); + checkIntArray((int[]) bean.get("intArray"), origArray); + intChanged[1] = 2; + checkIntArray(((TestBean) bean.get("nested")).getIntArray(), + intChanged); + + // Narrowing conversion required + BeanUtils.copyProperty(bean, "nested.intArray[1]", new Long((long) 3)); + checkIntArray((int[]) bean.get("intArray"), origArray); + intChanged[1] = 3; + checkIntArray(((TestBean) bean.get("nested")).getIntArray(), + intChanged); + + // String conversion required + BeanUtils.copyProperty(bean, "nested.intArray[1]", "4"); + checkIntArray((int[]) bean.get("intArray"), origArray); + intChanged[1] = 4; + checkIntArray(((TestBean) bean.get("nested")).getIntArray(), + intChanged); + + } + + + /** + * Test copying a property using a nested mapped map property. + */ + public void testCopyPropertyNestedMappedMap() throws Exception { + + Map origMap = new HashMap(); + origMap.put("First Key", "First Value"); + origMap.put("Second Key", "Second Value"); + Map changedMap = new HashMap(); + changedMap.put("First Key", "First Value"); + changedMap.put("Second Key", "Second Value"); + + // No conversion required + BeanUtils.copyProperty(bean, "nested.mapProperty(Second Key)", + "New Second Value"); + checkMap((Map) bean.get("mapProperty"), origMap); + changedMap.put("Second Key", "New Second Value"); + checkMap(((TestBean) bean.get("nested")).getMapProperty(), changedMap); + + } + + + /** + * Test copying a property using a nested simple expression, with and + * without conversions. + */ + public void testCopyPropertyNestedSimple() throws Exception { + + bean.set("intProperty", new Integer(0)); + nested.setIntProperty(0); + + // No conversion required + BeanUtils.copyProperty(bean, "nested.intProperty", new Integer(1)); + assertEquals(0, ((Integer) bean.get("intProperty")).intValue()); + assertEquals(1, nested.getIntProperty()); + + // Widening conversion required + BeanUtils.copyProperty(bean, "nested.intProperty", new Byte((byte) 2)); + assertEquals(0, ((Integer) bean.get("intProperty")).intValue()); + assertEquals(2, nested.getIntProperty()); + + // Narrowing conversion required + BeanUtils.copyProperty(bean, "nested.intProperty", new Long((long) 3)); + assertEquals(0, ((Integer) bean.get("intProperty")).intValue()); + assertEquals(3, nested.getIntProperty()); + + // String conversion required + BeanUtils.copyProperty(bean, "nested.intProperty", "4"); + assertEquals(0, ((Integer) bean.get("intProperty")).intValue()); + assertEquals(4, nested.getIntProperty()); + + } + + // ------------------------------------------------------ Protected Methods + // Ensure that the nested intArray matches the specified values + protected void checkIntArray(int actual[], int expected[]) { + assertNotNull("actual array not null", actual); + assertEquals("actual array length", expected.length, actual.length); + for (int i = 0; i < actual.length; i++) { + assertEquals("actual array value[" + i + "]", + expected[i], actual[i]); + } + } + + + // Ensure that the actual Map matches the expected Map + protected void checkMap(Map actual, Map expected) { + assertNotNull("actual map not null", actual); + assertEquals("actual map size", expected.size(), actual.size()); + Iterator keys = expected.keySet().iterator(); + while (keys.hasNext()) { + Object key = keys.next(); + assertEquals("actual map value(" + key + ")", + expected.get(key), actual.get(key)); + } + } + + /** * Create and return a DynaClass instance for our test * DynaBean. @@ -1063,6 +1189,7 @@ new DynaProperty("intProperty", Integer.TYPE), new DynaProperty("listIndexed", List.class), new DynaProperty("longProperty", Long.TYPE), + new DynaProperty("mapProperty", Map.class), new DynaProperty("mappedProperty", Map.class), new DynaProperty("mappedIntProperty", Map.class), new DynaProperty("nested", TestBean.class), --------------------------------------------------------------------- To unsubscribe, e-mail: commons-dev-unsubscribe@jakarta.apache.org For additional commands, e-mail: commons-dev-help@jakarta.apache.org