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. */
