This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-beanutils.git

commit 1d8d04e26fe11567eb99dd465329f3f8704d59fe
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Aug 23 09:47:17 2026 -0400

    Sort members.
---
 .../commons/beanutils2/PropertyUtilsBean.java      |  14 +-
 .../commons/beanutils2/LazyDynaListTest.java       | 246 ++++++++++-----------
 .../commons/beanutils2/PropertyUtilsTest.java      | 196 ++++++++--------
 3 files changed, 228 insertions(+), 228 deletions(-)

diff --git a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java 
b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java
index e9f98d5d..0309a15d 100644
--- a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java
+++ b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java
@@ -140,22 +140,22 @@ public class PropertyUtilsBean {
     }
 
     /**
-     * Clear any cached property descriptors information for all classes 
loaded by any class loaders. This is useful in cases where class loaders are 
thrown
-     * away to implement class reloading.
+     * Discards the memoized introspection results after the registered {@link 
BeanIntrospector} set has changed. Unlike {@link #clearDescriptors()} this 
leaves
+     * the JVM-global {@link Introspector} cache untouched.
      */
-    public void clearDescriptors() {
+    private void clearDescriptorCaches() {
         descriptorsCache.clear();
         mappedDescriptorsCache.clear();
-        Introspector.flushCaches();
     }
 
     /**
-     * Discards the memoized introspection results after the registered {@link 
BeanIntrospector} set has changed. Unlike {@link #clearDescriptors()} this 
leaves
-     * the JVM-global {@link Introspector} cache untouched.
+     * Clear any cached property descriptors information for all classes 
loaded by any class loaders. This is useful in cases where class loaders are 
thrown
+     * away to implement class reloading.
      */
-    private void clearDescriptorCaches() {
+    public void clearDescriptors() {
         descriptorsCache.clear();
         mappedDescriptorsCache.clear();
+        Introspector.flushCaches();
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/beanutils2/LazyDynaListTest.java 
b/src/test/java/org/apache/commons/beanutils2/LazyDynaListTest.java
index 52f1f67f..893cbd57 100644
--- a/src/test/java/org/apache/commons/beanutils2/LazyDynaListTest.java
+++ b/src/test/java/org/apache/commons/beanutils2/LazyDynaListTest.java
@@ -337,43 +337,6 @@ class LazyDynaListTest {
         dynaBeanTest(list, LazyDynaBean.class, bean.getDynaClass(), new 
BenchBean());
     }
 
-    /**
-     * Test Map Create
-     */
-    @Test
-    void testMapDynaClass() {
-
-        // Create LazyArrayList for TreeMap's
-        final LazyDynaList list = new LazyDynaList(treeMapDynaClass);
-
-        // test
-        mapTest(list, TreeMap.class, new BenchBean());
-
-    }
-
-    /**
-     * Test Map Create
-     */
-    @Test
-    void testMapType() {
-
-        // Create LazyArrayList for HashMap's
-        final LazyDynaList list = new LazyDynaList(HashMap.class);
-
-        // test
-        mapTest(list, HashMap.class, new BenchBean());
-
-    }
-
-    /**
-     * Test adding a map to List with no type set.
-     */
-    @Test
-    void testNullType() {
-        final LazyDynaList lazyList = new LazyDynaList();
-        lazyList.add(new HashMap<>());
-    }
-
     /**
      * Test that the element type is set from the first element populated on 
an untyped List: a later
      * element of a different type is rejected, and toArray() returns an array 
of the element type.
@@ -398,112 +361,40 @@ class LazyDynaListTest {
     }
 
     /**
-     * Test that a POJO first element on an untyped List takes the 
WrapDynaBean path: toArray()
-     * returns an array of the POJO class and toDynaBeanArray() returns a 
WrapDynaBean[].
+     * Test Map Create
      */
     @Test
-    void testUntypedListPojoFirstElement() {
-        final LazyDynaList lazyList = new LazyDynaList();
-        final TestBean bean = new TestBean();
-        lazyList.add(bean);
-
-        final Object[] array = lazyList.toArray();
-        assertEquals(TestBean.class, array.getClass().getComponentType(), "Not 
TestBean[]");
-        assertSame(bean, array[0], "Wrong element");
-
-        final DynaBean[] dynaArray = lazyList.toDynaBeanArray();
-        assertEquals(WrapDynaBean.class, 
dynaArray.getClass().getComponentType(), "Not WrapDynaBean[]");
-        assertSame(bean, ((WrapDynaBean) dynaArray[0]).getInstance(), "Wrong 
wrapped instance");
-    }
+    void testMapDynaClass() {
 
-    /**
-     * Test that a DynaBean first element on an untyped List sets both the 
element type and the
-     * DynaBean type to the same DynaBean subclass.
-     */
-    @Test
-    void testUntypedListDynaBeanFirstElement() throws Exception {
-        final LazyDynaList lazyList = new LazyDynaList();
-        final DynaBean bean = basicDynaClass.newInstance();
-        lazyList.add(bean);
+        // Create LazyArrayList for TreeMap's
+        final LazyDynaList list = new LazyDynaList(treeMapDynaClass);
 
-        // elementType: toArray() returns an array of the DynaBean subclass
-        final Object[] array = lazyList.toArray();
-        assertEquals(BasicDynaBean.class, array.getClass().getComponentType(), 
"Not BasicDynaBean[]");
-        assertSame(bean, array[0], "Wrong element");
+        // test
+        mapTest(list, TreeMap.class, new BenchBean());
 
-        // elementDynaBeanType: toDynaBeanArray() returns the same subclass
-        final DynaBean[] dynaArray = lazyList.toDynaBeanArray();
-        assertEquals(BasicDynaBean.class, 
dynaArray.getClass().getComponentType(), "Not BasicDynaBean[]");
-        assertSame(bean, dynaArray[0], "Wrong element");
     }
 
     /**
-     * Test addAll(Collection) and addAll(int, Collection) on an untyped List: 
the type is set from
-     * the first element of the Collection and mismatched types are then 
rejected.
+     * Test Map Create
      */
     @Test
-    void testUntypedListAddAll() {
-        final List<Object> collection = new ArrayList<>();
-        for (int i = 0; i < 2; i++) {
-            final TreeMap<String, Object> map = new TreeMap<>();
-            map.put("prop" + i, "val" + i);
-            collection.add(map);
-        }
-
-        // addAll(Collection)
-        final LazyDynaList lazyList = new LazyDynaList();
-        lazyList.addAll(collection);
-        assertEquals(2, lazyList.size(), "1. check size");
-        TreeMap<?, ?>[] mapArray = (TreeMap[]) lazyList.toArray();
-        assertEquals("val0", mapArray[0].get("prop0"), "2. Map error");
-        assertEquals("val1", mapArray[1].get("prop1"), "3. Map error");
-        assertThrows(IllegalArgumentException.class, () -> lazyList.add(new 
TestBean()), "4. wrong type accepted");
-
-        // addAll(int, Collection) - grows the List to the insert position 
first
-        final LazyDynaList indexedList = new LazyDynaList();
-        indexedList.addAll(2, collection);
-        assertEquals(4, indexedList.size(), "5. check size");
-        mapArray = (TreeMap[]) indexedList.toArray();
-        assertEquals(4, mapArray.length, "6. check size");
-        assertEquals("val0", mapArray[2].get("prop0"), "7. Map error");
-        assertEquals("val1", mapArray[3].get("prop1"), "8. Map error");
-        assertThrows(IllegalArgumentException.class, () -> indexedList.add(new 
TestBean()), "9. wrong type accepted");
-    }
+    void testMapType() {
 
-    /**
-     * Test that get(index) grows an untyped List with the element type fixed 
by the first
-     * population.
-     */
-    @Test
-    void testUntypedListGrowAfterFirstElement() {
-        final LazyDynaList lazyList = new LazyDynaList();
-        final TreeMap<String, Object> map = new TreeMap<>();
-        map.put("prop", "val");
-        lazyList.add(map);
+        // Create LazyArrayList for HashMap's
+        final LazyDynaList list = new LazyDynaList(HashMap.class);
 
-        final Object grown = lazyList.get(2);
-        assertNotNull(grown, "DynaBean Not Created");
-        assertEquals(LazyDynaMap.class, grown.getClass(), "Not LazyDynaMap");
-        assertEquals(TreeMap.class, ((LazyDynaMap) grown).getMap().getClass(), 
"Wrong Map");
-        assertEquals(3, lazyList.size(), "check size");
+        // test
+        mapTest(list, HashMap.class, new BenchBean());
 
-        final TreeMap<?, ?>[] mapArray = (TreeMap[]) lazyList.toArray();
-        assertEquals(3, mapArray.length, "check array size");
-        assertEquals("val", mapArray[0].get("prop"), "Map error");
     }
 
     /**
-     * Test toDynaBeanArray() type correctness for the untyped Map case.
+     * Test adding a map to List with no type set.
      */
     @Test
-    void testUntypedListToDynaBeanArray() {
+    void testNullType() {
         final LazyDynaList lazyList = new LazyDynaList();
         lazyList.add(new HashMap<>());
-
-        final DynaBean[] dynaArray = lazyList.toDynaBeanArray();
-        assertEquals(LazyDynaMap.class, 
dynaArray.getClass().getComponentType(), "Not LazyDynaMap[]");
-        assertEquals(1, dynaArray.length, "check size");
-        assertEquals(HashMap.class, ((LazyDynaMap) 
dynaArray[0]).getMap().getClass(), "Wrong Map");
     }
 
     /**
@@ -662,4 +553,113 @@ class LazyDynaListTest {
         assertEquals(1, array.length, "Wrong array size");
         assertEquals(elem, array[0], "Wrong element");
     }
+
+    /**
+     * Test addAll(Collection) and addAll(int, Collection) on an untyped List: 
the type is set from
+     * the first element of the Collection and mismatched types are then 
rejected.
+     */
+    @Test
+    void testUntypedListAddAll() {
+        final List<Object> collection = new ArrayList<>();
+        for (int i = 0; i < 2; i++) {
+            final TreeMap<String, Object> map = new TreeMap<>();
+            map.put("prop" + i, "val" + i);
+            collection.add(map);
+        }
+
+        // addAll(Collection)
+        final LazyDynaList lazyList = new LazyDynaList();
+        lazyList.addAll(collection);
+        assertEquals(2, lazyList.size(), "1. check size");
+        TreeMap<?, ?>[] mapArray = (TreeMap[]) lazyList.toArray();
+        assertEquals("val0", mapArray[0].get("prop0"), "2. Map error");
+        assertEquals("val1", mapArray[1].get("prop1"), "3. Map error");
+        assertThrows(IllegalArgumentException.class, () -> lazyList.add(new 
TestBean()), "4. wrong type accepted");
+
+        // addAll(int, Collection) - grows the List to the insert position 
first
+        final LazyDynaList indexedList = new LazyDynaList();
+        indexedList.addAll(2, collection);
+        assertEquals(4, indexedList.size(), "5. check size");
+        mapArray = (TreeMap[]) indexedList.toArray();
+        assertEquals(4, mapArray.length, "6. check size");
+        assertEquals("val0", mapArray[2].get("prop0"), "7. Map error");
+        assertEquals("val1", mapArray[3].get("prop1"), "8. Map error");
+        assertThrows(IllegalArgumentException.class, () -> indexedList.add(new 
TestBean()), "9. wrong type accepted");
+    }
+
+    /**
+     * Test that a DynaBean first element on an untyped List sets both the 
element type and the
+     * DynaBean type to the same DynaBean subclass.
+     */
+    @Test
+    void testUntypedListDynaBeanFirstElement() throws Exception {
+        final LazyDynaList lazyList = new LazyDynaList();
+        final DynaBean bean = basicDynaClass.newInstance();
+        lazyList.add(bean);
+
+        // elementType: toArray() returns an array of the DynaBean subclass
+        final Object[] array = lazyList.toArray();
+        assertEquals(BasicDynaBean.class, array.getClass().getComponentType(), 
"Not BasicDynaBean[]");
+        assertSame(bean, array[0], "Wrong element");
+
+        // elementDynaBeanType: toDynaBeanArray() returns the same subclass
+        final DynaBean[] dynaArray = lazyList.toDynaBeanArray();
+        assertEquals(BasicDynaBean.class, 
dynaArray.getClass().getComponentType(), "Not BasicDynaBean[]");
+        assertSame(bean, dynaArray[0], "Wrong element");
+    }
+
+    /**
+     * Test that get(index) grows an untyped List with the element type fixed 
by the first
+     * population.
+     */
+    @Test
+    void testUntypedListGrowAfterFirstElement() {
+        final LazyDynaList lazyList = new LazyDynaList();
+        final TreeMap<String, Object> map = new TreeMap<>();
+        map.put("prop", "val");
+        lazyList.add(map);
+
+        final Object grown = lazyList.get(2);
+        assertNotNull(grown, "DynaBean Not Created");
+        assertEquals(LazyDynaMap.class, grown.getClass(), "Not LazyDynaMap");
+        assertEquals(TreeMap.class, ((LazyDynaMap) grown).getMap().getClass(), 
"Wrong Map");
+        assertEquals(3, lazyList.size(), "check size");
+
+        final TreeMap<?, ?>[] mapArray = (TreeMap[]) lazyList.toArray();
+        assertEquals(3, mapArray.length, "check array size");
+        assertEquals("val", mapArray[0].get("prop"), "Map error");
+    }
+
+    /**
+     * Test that a POJO first element on an untyped List takes the 
WrapDynaBean path: toArray()
+     * returns an array of the POJO class and toDynaBeanArray() returns a 
WrapDynaBean[].
+     */
+    @Test
+    void testUntypedListPojoFirstElement() {
+        final LazyDynaList lazyList = new LazyDynaList();
+        final TestBean bean = new TestBean();
+        lazyList.add(bean);
+
+        final Object[] array = lazyList.toArray();
+        assertEquals(TestBean.class, array.getClass().getComponentType(), "Not 
TestBean[]");
+        assertSame(bean, array[0], "Wrong element");
+
+        final DynaBean[] dynaArray = lazyList.toDynaBeanArray();
+        assertEquals(WrapDynaBean.class, 
dynaArray.getClass().getComponentType(), "Not WrapDynaBean[]");
+        assertSame(bean, ((WrapDynaBean) dynaArray[0]).getInstance(), "Wrong 
wrapped instance");
+    }
+
+    /**
+     * Test toDynaBeanArray() type correctness for the untyped Map case.
+     */
+    @Test
+    void testUntypedListToDynaBeanArray() {
+        final LazyDynaList lazyList = new LazyDynaList();
+        lazyList.add(new HashMap<>());
+
+        final DynaBean[] dynaArray = lazyList.toDynaBeanArray();
+        assertEquals(LazyDynaMap.class, 
dynaArray.getClass().getComponentType(), "Not LazyDynaMap[]");
+        assertEquals(1, dynaArray.length, "check size");
+        assertEquals(HashMap.class, ((LazyDynaMap) 
dynaArray[0]).getMap().getClass(), "Wrong Map");
+    }
 }
diff --git a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java 
b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java
index 0d1753d1..9e23278d 100644
--- a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java
+++ b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java
@@ -199,6 +199,30 @@ class PropertyUtilsTest {
         PropertyUtils.resetBeanIntrospectors();
     }
 
+    /**
+     * Registering a {@link SuppressPropertiesBeanIntrospector} must take 
effect for classes already introspected. The per-class descriptor cache was 
populated
+     * before the introspector was added and was never invalidated, so a 
property suppressed for hardening stayed readable and writable when its class 
had been
+     * introspected earlier (the common case with the shared {@code 
PropertyUtils} singleton).
+     */
+    @Test
+    void testAddBeanIntrospectorInvalidatesCache() throws Exception {
+        final PropertyUtilsBean pub = new PropertyUtilsBean();
+
+        // Warm the descriptor cache for TestBean before the suppression is 
configured.
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be visible before suppression");
+
+        final SuppressPropertiesBeanIntrospector suppressor = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty"));
+        pub.addBeanIntrospector(suppressor);
+
+        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Suppressed property should have no descriptor after the introspector is 
added");
+        assertThrows(NoSuchMethodException.class, () -> pub.getProperty(bean, 
"stringProperty"), "Suppressed property must not be readable");
+        assertThrows(NoSuchMethodException.class, () -> pub.setProperty(bean, 
"stringProperty", "changed"), "Suppressed property must not be writable");
+
+        // Removing the introspector must likewise invalidate the cache so the 
property becomes visible again.
+        pub.removeBeanIntrospector(suppressor);
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be visible again after the introspector is removed");
+    }
+
     /**
      * Tries to add a null BeanIntrospector.
      */
@@ -347,104 +371,6 @@ class PropertyUtilsTest {
         assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), "A 
null suppressed entry must not hide unrelated properties");
     }
 
-    /**
-     * Registering a {@link SuppressPropertiesBeanIntrospector} must take 
effect for classes already introspected. The per-class descriptor cache was 
populated
-     * before the introspector was added and was never invalidated, so a 
property suppressed for hardening stayed readable and writable when its class 
had been
-     * introspected earlier (the common case with the shared {@code 
PropertyUtils} singleton).
-     */
-    @Test
-    void testAddBeanIntrospectorInvalidatesCache() throws Exception {
-        final PropertyUtilsBean pub = new PropertyUtilsBean();
-
-        // Warm the descriptor cache for TestBean before the suppression is 
configured.
-        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be visible before suppression");
-
-        final SuppressPropertiesBeanIntrospector suppressor = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty"));
-        pub.addBeanIntrospector(suppressor);
-
-        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Suppressed property should have no descriptor after the introspector is 
added");
-        assertThrows(NoSuchMethodException.class, () -> pub.getProperty(bean, 
"stringProperty"), "Suppressed property must not be readable");
-        assertThrows(NoSuchMethodException.class, () -> pub.setProperty(bean, 
"stringProperty", "changed"), "Suppressed property must not be writable");
-
-        // Removing the introspector must likewise invalidate the cache so the 
property becomes visible again.
-        pub.removeBeanIntrospector(suppressor);
-        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be visible again after the introspector is removed");
-    }
-
-    /**
-     * {@code resetBeanIntrospectors()} must invalidate the descriptor caches 
so that introspection results produced by a previously registered custom
-     * introspector are re-evaluated against the restored default set.
-     */
-    @Test
-    void testResetBeanIntrospectorsInvalidatesCache() throws Exception {
-        final PropertyUtilsBean pub = new PropertyUtilsBean();
-        pub.addBeanIntrospector(new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty")));
-
-        // Warm the descriptor cache with the suppression in effect.
-        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Suppressed property should have no descriptor");
-
-        pub.resetBeanIntrospectors();
-
-        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be re-evaluated and visible after the reset");
-        assertEquals(bean.getStringProperty(), pub.getProperty(bean, 
"stringProperty"), "Property should be readable after the reset");
-    }
-
-    /**
-     * Changing the introspector set must also invalidate the mapped 
descriptor cache. The suppression guard in {@code getPropertyDescriptor} 
already hides a
-     * suppressed mapped property, so this checks the cached {@link 
MappedPropertyDescriptor} itself is dropped and re-created rather than served 
stale.
-     */
-    @Test
-    void testIntrospectorChangeInvalidatesMappedDescriptorCache() throws 
Exception {
-        final PropertyUtilsBean pub = new PropertyUtilsBean();
-
-        // Warm the mapped descriptor cache for TestBean.
-        final PropertyDescriptor first = pub.getPropertyDescriptor(bean, 
"mappedProperty");
-        assertNotNull(first, "Mapped property should be visible before 
suppression");
-
-        final SuppressPropertiesBeanIntrospector suppressor = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("mappedProperty"));
-        pub.addBeanIntrospector(suppressor);
-
-        assertNull(pub.getPropertyDescriptor(bean, "mappedProperty"), 
"Suppressed mapped property should have no descriptor");
-        assertThrows(NoSuchMethodException.class, () -> pub.getProperty(bean, 
"mappedProperty(First Key)"), "Suppressed mapped property must not be 
readable");
-
-        pub.removeBeanIntrospector(suppressor);
-
-        final PropertyDescriptor second = pub.getPropertyDescriptor(bean, 
"mappedProperty");
-        assertNotNull(second, "Mapped property should be visible again after 
the introspector is removed");
-        assertNotSame(first, second, "Mapped descriptor must be re-created, 
not served from the stale cache");
-        assertEquals("First Value", pub.getProperty(bean, 
"mappedProperty(First Key)"), "Mapped property should be readable again");
-    }
-
-    /**
-     * Each add and remove in a sequence of introspector changes must 
invalidate the caches, so the visible property set always reflects the currently
-     * registered introspectors.
-     */
-    @Test
-    void testSequentialIntrospectorChangesInvalidateCache() throws Exception {
-        final PropertyUtilsBean pub = new PropertyUtilsBean();
-        final SuppressPropertiesBeanIntrospector suppressString = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty"));
-        final SuppressPropertiesBeanIntrospector suppressInt = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("intProperty"));
-
-        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be visible initially");
-        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be visible initially");
-
-        pub.addBeanIntrospector(suppressString);
-        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be suppressed after the first add");
-        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be unaffected by the first add");
-
-        pub.addBeanIntrospector(suppressInt);
-        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should stay suppressed after the second add");
-        assertNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be suppressed after the second add");
-
-        pub.removeBeanIntrospector(suppressString);
-        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be visible again after its introspector is removed");
-        assertNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should stay suppressed after the unrelated remove");
-
-        pub.removeBeanIntrospector(suppressInt);
-        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should stay visible after the last remove");
-        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be visible again after its introspector is removed");
-    }
-
     /**
      * Test the describe() method.
      */
@@ -1572,6 +1498,32 @@ class PropertyUtilsTest {
         testGetWriteMethod(beanPublicSubclass, properties, TEST_BEAN_CLASS);
     }
 
+    /**
+     * Changing the introspector set must also invalidate the mapped 
descriptor cache. The suppression guard in {@code getPropertyDescriptor} 
already hides a
+     * suppressed mapped property, so this checks the cached {@link 
MappedPropertyDescriptor} itself is dropped and re-created rather than served 
stale.
+     */
+    @Test
+    void testIntrospectorChangeInvalidatesMappedDescriptorCache() throws 
Exception {
+        final PropertyUtilsBean pub = new PropertyUtilsBean();
+
+        // Warm the mapped descriptor cache for TestBean.
+        final PropertyDescriptor first = pub.getPropertyDescriptor(bean, 
"mappedProperty");
+        assertNotNull(first, "Mapped property should be visible before 
suppression");
+
+        final SuppressPropertiesBeanIntrospector suppressor = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("mappedProperty"));
+        pub.addBeanIntrospector(suppressor);
+
+        assertNull(pub.getPropertyDescriptor(bean, "mappedProperty"), 
"Suppressed mapped property should have no descriptor");
+        assertThrows(NoSuchMethodException.class, () -> pub.getProperty(bean, 
"mappedProperty(First Key)"), "Suppressed mapped property must not be 
readable");
+
+        pub.removeBeanIntrospector(suppressor);
+
+        final PropertyDescriptor second = pub.getPropertyDescriptor(bean, 
"mappedProperty");
+        assertNotNull(second, "Mapped property should be visible again after 
the introspector is removed");
+        assertNotSame(first, second, "Mapped descriptor must be re-created, 
not served from the stale cache");
+        assertEquals("First Value", pub.getProperty(bean, 
"mappedProperty(First Key)"), "Mapped property should be readable again");
+    }
+
     /**
      * Test isReadable() method.
      */
@@ -1841,6 +1793,54 @@ class PropertyUtilsTest {
         assertTrue(desc.length > 0, "Got no descriptors");
     }
 
+    /**
+     * {@code resetBeanIntrospectors()} must invalidate the descriptor caches 
so that introspection results produced by a previously registered custom
+     * introspector are re-evaluated against the restored default set.
+     */
+    @Test
+    void testResetBeanIntrospectorsInvalidatesCache() throws Exception {
+        final PropertyUtilsBean pub = new PropertyUtilsBean();
+        pub.addBeanIntrospector(new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty")));
+
+        // Warm the descriptor cache with the suppression in effect.
+        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Suppressed property should have no descriptor");
+
+        pub.resetBeanIntrospectors();
+
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be re-evaluated and visible after the reset");
+        assertEquals(bean.getStringProperty(), pub.getProperty(bean, 
"stringProperty"), "Property should be readable after the reset");
+    }
+
+    /**
+     * Each add and remove in a sequence of introspector changes must 
invalidate the caches, so the visible property set always reflects the currently
+     * registered introspectors.
+     */
+    @Test
+    void testSequentialIntrospectorChangesInvalidateCache() throws Exception {
+        final PropertyUtilsBean pub = new PropertyUtilsBean();
+        final SuppressPropertiesBeanIntrospector suppressString = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty"));
+        final SuppressPropertiesBeanIntrospector suppressInt = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("intProperty"));
+
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be visible initially");
+        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be visible initially");
+
+        pub.addBeanIntrospector(suppressString);
+        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be suppressed after the first add");
+        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be unaffected by the first add");
+
+        pub.addBeanIntrospector(suppressInt);
+        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should stay suppressed after the second add");
+        assertNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be suppressed after the second add");
+
+        pub.removeBeanIntrospector(suppressString);
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be visible again after its introspector is removed");
+        assertNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should stay suppressed after the unrelated remove");
+
+        pub.removeBeanIntrospector(suppressInt);
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should stay visible after the last remove");
+        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be visible again after its introspector is removed");
+    }
+
     /**
      * Corner cases on setIndexedProperty invalid arguments.
      */

Reply via email to