diff --git a/src/main/java/org/apache/commons/beanutils2/MappedPropertyDescriptor.java b/src/main/java/org/apache/commons/beanutils2/MappedPropertyDescriptor.java index 21d69d464..9a5b07e49 100644 --- a/src/main/java/org/apache/commons/beanutils2/MappedPropertyDescriptor.java +++ b/src/main/java/org/apache/commons/beanutils2/MappedPropertyDescriptor.java @@ -162,7 +162,7 @@ private Class reLoadClass(final String name) { * * @param s The property name */ - private static String capitalizePropertyName(final String s) { + static String capitalizePropertyName(final String s) { if (s.isEmpty()) { return s; } diff --git a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java index be0f30665..36c653f07 100644 --- a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java +++ b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java @@ -1068,15 +1068,21 @@ private Object invokeMethod(final Method method, final Object bean, final Object /** * Tests whether a property name has been removed by a registered {@link SuppressPropertiesBeanIntrospector}. The mapped-descriptor fallback in * {@link #getPropertyDescriptor(Object, String)} bypasses the introspection pipeline, so suppressed mapped property names must be filtered explicitly. + * {@link MappedPropertyDescriptor} derives its accessor names from the capitalized property name, so names differing only in the case of their first + * character resolve the same accessors and are compared on that capitalized form here. * * @param name The property name to test. * @return {@code true} if the name is suppressed by an introspector. */ private boolean isPropertySuppressed(final String name) { + final String base = MappedPropertyDescriptor.capitalizePropertyName(name); for (final BeanIntrospector introspector : introspectors) { - if (introspector instanceof SuppressPropertiesBeanIntrospector - && ((SuppressPropertiesBeanIntrospector) introspector).getSuppressedProperties().contains(name)) { - return true; + if (introspector instanceof SuppressPropertiesBeanIntrospector) { + for (final String suppressed : ((SuppressPropertiesBeanIntrospector) introspector).getSuppressedProperties()) { + if (suppressed != null && base.equals(MappedPropertyDescriptor.capitalizePropertyName(suppressed))) { + return true; + } + } } } return false; diff --git a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java index 683389508..8e10a0ecf 100644 --- a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java +++ b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java @@ -315,6 +315,37 @@ void testCustomIntrospectionSuppressedMappedProperty() throws Exception { assertEquals("First Value", unsuppressed.getProperty(bean, "mappedProperty(First Key)"), "Mapped property should be readable when not suppressed"); } + /** + * {@link MappedPropertyDescriptor} capitalizes the first character of the property name to build the accessor names, so {@code MappedProperty(key)} + * resolves the same {@code getMappedProperty}/{@code setMappedProperty} pair as {@code mappedProperty(key)} and must be suppressed with it. + */ + @Test + void testCustomIntrospectionSuppressedMappedPropertyCaseVariant() throws Exception { + final PropertyUtilsBean pub = new PropertyUtilsBean(); + pub.addBeanIntrospector(new SuppressPropertiesBeanIntrospector(Arrays.asList("mappedProperty"))); + + assertNull(pub.getPropertyDescriptor(bean, "MappedProperty"), "Case variant of a suppressed mapped property should have no descriptor"); + assertThrows(NoSuchMethodException.class, () -> pub.getProperty(bean, "MappedProperty(First Key)"), + "Case variant of a suppressed mapped property must not be readable"); + assertThrows(NoSuchMethodException.class, () -> pub.setProperty(bean, "MappedProperty(First Key)", "changed"), + "Case variant of a suppressed mapped property must not be writable"); + assertEquals("First Value", bean.getMappedProperty("First Key"), "Suppressed mapped property must be unchanged"); + } + + /** + * {@link SuppressPropertiesBeanIntrospector} only rejects a null collection, so the set of suppressed names may hold null entries. Those must be skipped + * rather than capitalized when the mapped-descriptor fallback is checked. + */ + @Test + void testCustomIntrospectionSuppressedMappedPropertyNullEntry() throws Exception { + final PropertyUtilsBean pub = new PropertyUtilsBean(); + pub.addBeanIntrospector(new SuppressPropertiesBeanIntrospector(Arrays.asList(null, "mappedProperty"))); + + assertNull(pub.getPropertyDescriptor(bean, "MappedProperty"), "Case variant of a suppressed mapped property should have no descriptor"); + assertNull(pub.getPropertyDescriptor(bean, "mappedProperty"), "Suppressed mapped property should have no descriptor"); + assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), "A null suppressed entry must not hide unrelated properties"); + } + /** * Test the describe() method. */