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

markt-asf pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/9.0.x by this push:
     new a9b9b28f6a Review "Fix name-scoped removal of attribute change 
notification ..."
a9b9b28f6a is described below

commit a9b9b28f6a29ca57128d7fa0707b33faa3a490e8
Author: Mark Thomas <[email protected]>
AuthorDate: Tue Nov 3 16:52:17 2026 +0000

    Review "Fix name-scoped removal of attribute change notification ..."
    
    The Javadoc for ModelMBeanNotificationBroadcaster.html#
        removeAttributeChangeNotificationListener(
            javax.management.NotificationListener,java.lang.String)
    requires a null attribute name to remove the listener from all
    registrations.
    
    Don't modify filters in place. Handle filter sub-classes (unlikely)
    
    Co-authored by: Claude / Sonnet 5.5
---
 .../util/modeler/BaseNotificationBroadcaster.java  | 104 +++++++---
 .../modeler/TestBaseModelMBeanNotifications.java   | 212 +++++++++++++++++++++
 2 files changed, 294 insertions(+), 22 deletions(-)

diff --git 
a/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java 
b/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java
index dcd0224012..a57f20cd6f 100644
--- a/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java
+++ b/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java
@@ -18,8 +18,11 @@ package org.apache.tomcat.util.modeler;
 
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Set;
 
+import javax.management.AttributeChangeNotification;
 import javax.management.ListenerNotFoundException;
 import javax.management.MBeanNotificationInfo;
 import javax.management.Notification;
@@ -71,23 +74,33 @@ public class BaseNotificationBroadcaster implements 
NotificationBroadcaster {
 
         synchronized (entries) {
 
-            // Optimization to coalesce attribute name filters
-            if (filter instanceof BaseAttributeFilter) {
+            /*
+             * Optimization to coalesce attribute name filters. The filters 
are never modified. A filter may be shared
+             * with other registrations or broadcasters so the entry is given 
a new filter instead. Only plain
+             * BaseAttributeFilter instances are coalesced. A sub-class may 
change how notifications are filtered, which
+             * a plain filter would not preserve.
+             */
+            if (isPlainAttributeFilter(filter)) {
                 BaseAttributeFilter newFilter = (BaseAttributeFilter) filter;
                 for (BaseNotificationBroadcasterEntry item : entries) {
-                    if ((item.listener == listener) && (item.filter instanceof 
BaseAttributeFilter) &&
+                    if ((item.listener == listener) && 
isPlainAttributeFilter(item.filter) &&
                             (item.handback == handback)) {
-                        BaseAttributeFilter oldFilter = (BaseAttributeFilter) 
item.filter;
                         String[] newNames = newFilter.getNames();
-                        String[] oldNames = oldFilter.getNames();
+                        String[] oldNames = ((BaseAttributeFilter) 
item.filter).getNames();
                         if (newNames.length == 0) {
-                            oldFilter.clear();
-                        } else {
                             if (oldNames.length != 0) {
-                                for (String newName : newNames) {
-                                    oldFilter.addAttribute(newName);
-                                }
+                                // Now accepts all attribute names
+                                item.filter = new BaseAttributeFilter(null);
                             }
+                        } else if (oldNames.length != 0) {
+                            BaseAttributeFilter combined = new 
BaseAttributeFilter(null);
+                            for (String oldName : oldNames) {
+                                combined.addAttribute(oldName);
+                            }
+                            for (String newName : newNames) {
+                                combined.addAttribute(newName);
+                            }
+                            item.filter = combined;
                         }
                         return;
                     }
@@ -128,7 +141,8 @@ public class BaseNotificationBroadcaster implements 
NotificationBroadcaster {
 
 
     /**
-     * Remove an attribute change notification event listener for a specific 
attribute name from this MBean. Other
+     * Remove an attribute change notification event listener for a specific 
attribute name from this MBean, or all
+     * attribute change registrations for the listener if the name is 
<code>null</code>. For a specific name, other
      * registrations of the same listener are left untouched. If a coalesced 
filter ends up accepting no explicit
      * names, the corresponding entry is removed entirely rather than being 
turned into an accept-all filter. A
      * registration that accepts all attribute names (a filter with no 
explicit names, created when the listener was
@@ -136,7 +150,7 @@ public class BaseNotificationBroadcaster implements 
NotificationBroadcaster {
      *
      * @param listener The listener to be removed
      * @param name     The attribute name for which the registration should be 
removed, or <code>null</code> to remove
-     *                     a registration that accepts all attribute names
+     *                     all attribute change registrations for the listener
      */
     public void removeAttributeNotificationListener(NotificationListener 
listener, String name) {
 
@@ -144,20 +158,39 @@ public class BaseNotificationBroadcaster implements 
NotificationBroadcaster {
             Iterator<BaseNotificationBroadcasterEntry> iterator = 
entries.iterator();
             while (iterator.hasNext()) {
                 BaseNotificationBroadcasterEntry item = iterator.next();
-                if (item.listener != listener || !(item.filter instanceof 
BaseAttributeFilter)) {
+                if (item.listener != listener) {
                     continue;
                 }
-                BaseAttributeFilter filter = (BaseAttributeFilter) item.filter;
-                String[] names = filter.getNames();
                 if (name == null) {
-                    if (names.length == 0) {
-                        // The accept-all registration
+                    iterator.remove();
+                } else if (item.filter instanceof BaseAttributeFilter) {
+                    BaseAttributeFilter filter = (BaseAttributeFilter) 
item.filter;
+                    String[] names = filter.getNames();
+                    if (!isPlainAttributeFilter(filter)) {
+                        if (names.length > 0) {
+                            // Keep the subclass's filtering behavior and its 
shared state intact. The subclass may
+                            // accept names beyond those reported by 
getNames(), so exclude only the requested name
+                            // from this entry rather than replacing the 
filter or removing the entry.
+                            item.excludeAttribute(name);
+                        }
+                    } else if (names.length == 1 && name.equals(names[0])) {
                         iterator.remove();
+                    } else if (names.length > 1) {
+                        // The filter may be shared with other registrations 
or broadcasters. Replace this entry's
+                        // filter rather than changing the shared filter.
+                        BaseAttributeFilter replacement = new 
BaseAttributeFilter(null);
+                        boolean removed = false;
+                        for (String attributeName : names) {
+                            if (name.equals(attributeName)) {
+                                removed = true;
+                            } else {
+                                replacement.addAttribute(attributeName);
+                            }
+                        }
+                        if (removed) {
+                            item.filter = replacement;
+                        }
                     }
-                } else if (names.length == 1 && names[0].equals(name)) {
-                    iterator.remove();
-                } else {
-                    filter.removeAttribute(name);
                 }
             }
         }
@@ -165,6 +198,15 @@ public class BaseNotificationBroadcaster implements 
NotificationBroadcaster {
     }
 
 
+    /**
+     * Is the filter a plain <code>BaseAttributeFilter</code> rather than an 
instance of a sub-class, which may change
+     * how notifications are filtered?
+     */
+    private static boolean isPlainAttributeFilter(NotificationFilter filter) {
+        return filter != null && filter.getClass() == 
BaseAttributeFilter.class;
+    }
+
+
     /**
      * Send the specified notification to all interested listeners.
      *
@@ -174,7 +216,7 @@ public class BaseNotificationBroadcaster implements 
NotificationBroadcaster {
 
         synchronized (entries) {
             for (BaseNotificationBroadcasterEntry item : entries) {
-                if ((item.filter != null) && 
(!item.filter.isNotificationEnabled(notification))) {
+                if (!item.isNotificationEnabled(notification)) {
                     continue;
                 }
                 item.listener.handleNotification(notification, item.handback);
@@ -204,4 +246,22 @@ class BaseNotificationBroadcasterEntry {
 
     public NotificationListener listener;
 
+    // Accessed only while holding the broadcaster's entries lock.
+    private Set<String> excludedAttributes;
+
+    void excludeAttribute(String name) {
+        if (excludedAttributes == null) {
+            excludedAttributes = new HashSet<>();
+        }
+        excludedAttributes.add(name);
+    }
+
+    boolean isNotificationEnabled(Notification notification) {
+        if (excludedAttributes != null && notification instanceof 
AttributeChangeNotification &&
+                excludedAttributes.contains(((AttributeChangeNotification) 
notification).getAttributeName())) {
+            return false;
+        }
+        return filter == null || filter.isNotificationEnabled(notification);
+    }
+
 }
diff --git 
a/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java 
b/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
index c851f96fce..0e074966d2 100644
--- a/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
+++ b/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
@@ -131,6 +131,218 @@ public class TestBaseModelMBeanNotifications {
         Assert.assertTrue(listener.received.isEmpty());
     }
 
+    @Test
+    public void testNullNameRemovalRemovesAllAttributeRegistrations() throws 
Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener = new TesterNotificationListener();
+        TesterNotificationListener otherListener = new 
TesterNotificationListener();
+        // Cover coalesced names, a separate handback and an accept-all 
registration.
+        mbean.addAttributeChangeNotificationListener(listener, "A", "hb1");
+        mbean.addAttributeChangeNotificationListener(listener, "B", "hb1");
+        mbean.addAttributeChangeNotificationListener(listener, "C", "hb2");
+        mbean.addAttributeChangeNotificationListener(listener, null, "hb3");
+        mbean.addAttributeChangeNotificationListener(otherListener, "A", 
"other");
+
+        mbean.removeAttributeChangeNotificationListener(listener, null);
+        send(mbean, "A");
+        send(mbean, "B");
+        send(mbean, "C");
+        send(mbean, "D");
+
+        Assert.assertTrue(listener.received.isEmpty());
+        Assert.assertEquals(List.of("A:other"), otherListener.received);
+    }
+
+    @Test
+    public void testNullNameRemovalKeepsGeneralNotifications() throws 
Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener = new TesterNotificationListener();
+        mbean.addNotificationListener(listener, notification -> true, "hb");
+
+        mbean.removeAttributeChangeNotificationListener(listener, null);
+        send(mbean, "A");
+        Assert.assertTrue(listener.received.isEmpty());
+
+        mbean.sendNotification("message");
+        Assert.assertEquals(List.of("jmx.modelmbean.generic:hb"), 
listener.received);
+    }
+
+    @Test
+    public void testSharedFilterRemovalKeepsOtherListener() throws Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener1 = new 
TesterNotificationListener();
+        TesterNotificationListener listener2 = new 
TesterNotificationListener();
+        BaseAttributeFilter filter = new BaseAttributeFilter("A");
+        filter.addAttribute("B");
+        // The same filter may be supplied for independent registrations.
+        mbean.addNotificationListener(listener1, filter, "hb1");
+        mbean.addNotificationListener(listener2, filter, "hb2");
+
+        mbean.removeAttributeChangeNotificationListener(listener1, "A");
+        send(mbean, "A");
+        send(mbean, "B");
+
+        Assert.assertEquals(List.of("B:hb1"), listener1.received);
+        Assert.assertEquals(List.of("A:hb2", "B:hb2"), listener2.received);
+    }
+
+    /*
+     * A filter may be shared between registrations 
(BaseModelMBean.addNotificationListener() uses the same filter for
+     * the general and the attribute registration). Coalescing registrations 
must not change the shared filter.
+     */
+    @Test
+    public void testCoalescingNamesDoesNotChangeSharedFilter() throws 
Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener1 = new 
TesterNotificationListener();
+        TesterNotificationListener listener2 = new 
TesterNotificationListener();
+        BaseAttributeFilter filter = new BaseAttributeFilter("A");
+        mbean.addNotificationListener(listener1, filter, "hb1");
+        mbean.addNotificationListener(listener2, filter, "hb2");
+
+        // Same listener and handback so the registrations are coalesced
+        mbean.addAttributeChangeNotificationListener(listener1, "B", "hb1");
+        send(mbean, "A");
+        send(mbean, "B");
+
+        Assert.assertEquals(List.of("A:hb1", "B:hb1"), listener1.received);
+        Assert.assertEquals(List.of("A:hb2"), listener2.received);
+        Assert.assertArrayEquals(new String[] { "A" }, filter.getNames());
+    }
+
+    @Test
+    public void testCoalescingAcceptAllDoesNotChangeSharedFilter() throws 
Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener1 = new 
TesterNotificationListener();
+        TesterNotificationListener listener2 = new 
TesterNotificationListener();
+        BaseAttributeFilter filter = new BaseAttributeFilter("A");
+        mbean.addNotificationListener(listener1, filter, "hb1");
+        mbean.addNotificationListener(listener2, filter, "hb2");
+
+        mbean.addAttributeChangeNotificationListener(listener1, null, "hb1");
+        send(mbean, "Z");
+
+        Assert.assertEquals(List.of("Z:hb1"), listener1.received);
+        Assert.assertTrue(listener2.received.isEmpty());
+        Assert.assertArrayEquals(new String[] { "A" }, filter.getNames());
+    }
+
+    @Test
+    public void testCoalescingKeepsAcceptAll() throws Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener = new TesterNotificationListener();
+        mbean.addAttributeChangeNotificationListener(listener, null, "hb");
+        // The accept-all registration is not narrowed by a later named 
registration
+        mbean.addAttributeChangeNotificationListener(listener, "A", "hb");
+        send(mbean, "Z");
+
+        Assert.assertEquals(List.of("Z:hb"), listener.received);
+    }
+
+    /*
+     * A sub-class of the filter may change how notifications are filtered. 
Its registration is never coalesced with
+     * another registration as that would replace it with a plain filter.
+     */
+    @Test
+    public void testSubclassFilterIsNotCoalesced() throws Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener = new TesterNotificationListener();
+        BaseAttributeFilter rejectAll = new BaseAttributeFilter("A") {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public boolean isNotificationEnabled(Notification notification) {
+                return false;
+            }
+        };
+        mbean.addNotificationListener(listener, rejectAll, "hb");
+        mbean.addAttributeChangeNotificationListener(listener, "B", "hb");
+        send(mbean, "A");
+        send(mbean, "B");
+
+        Assert.assertEquals(List.of("B:hb"), listener.received);
+    }
+
+    @Test
+    public void testSharedSubclassFilterRemovalKeepsOtherListener() throws 
Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener1 = new 
TesterNotificationListener();
+        TesterNotificationListener listener2 = new 
TesterNotificationListener();
+        BaseAttributeFilter filter = new BaseAttributeFilter("A") {
+            private static final long serialVersionUID = 1L;
+        };
+        filter.addAttribute("B");
+        mbean.addNotificationListener(listener1, filter, "hb1");
+        mbean.addNotificationListener(listener2, filter, "hb2");
+
+        mbean.removeAttributeChangeNotificationListener(listener1, "A");
+        send(mbean, "A");
+        send(mbean, "B");
+        Assert.assertEquals(List.of("B:hb1"), listener1.received);
+        Assert.assertEquals(List.of("A:hb2", "B:hb2"), listener2.received);
+
+        // Removing the remaining name must not turn the registration into an 
accept-all filter.
+        listener1.received.clear();
+        listener2.received.clear();
+        mbean.removeAttributeChangeNotificationListener(listener1, "B");
+        send(mbean, "A");
+        send(mbean, "B");
+        send(mbean, "C");
+        Assert.assertTrue(listener1.received.isEmpty());
+        Assert.assertEquals(List.of("A:hb2", "B:hb2"), listener2.received);
+    }
+
+    @Test
+    public void testSubclassFilterRemovalPreservesCustomFiltering() throws 
Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener = new TesterNotificationListener();
+        BaseAttributeFilter filter = new BaseAttributeFilter("A") {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public boolean isNotificationEnabled(Notification notification) {
+                return super.isNotificationEnabled(notification) &&
+                        !"C".equals(((AttributeChangeNotification) 
notification).getAttributeName());
+            }
+        };
+        filter.addAttribute("B");
+        filter.addAttribute("C");
+        mbean.addNotificationListener(listener, filter, "hb");
+
+        mbean.removeAttributeChangeNotificationListener(listener, "A");
+        send(mbean, "A");
+        send(mbean, "B");
+        send(mbean, "C");
+        Assert.assertEquals(List.of("B:hb"), listener.received);
+
+        // Changes to the original subclass filter still apply to the 
registration.
+        filter.addAttribute("D");
+        send(mbean, "D");
+        Assert.assertEquals(List.of("B:hb", "D:hb"), listener.received);
+    }
+
+    @Test
+    public void testSubclassFilterRemovalKeepsOtherAcceptedNames() throws 
Exception {
+        BaseModelMBean mbean = new BaseModelMBean();
+        TesterNotificationListener listener = new TesterNotificationListener();
+        BaseAttributeFilter filter = new BaseAttributeFilter("A") {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public boolean isNotificationEnabled(Notification notification) {
+                return notification instanceof AttributeChangeNotification &&
+                        ("A".equals(((AttributeChangeNotification) 
notification).getAttributeName()) ||
+                                "B".equals(((AttributeChangeNotification) 
notification).getAttributeName()));
+            }
+        };
+        mbean.addNotificationListener(listener, filter, "hb");
+
+        // A subclass may accept more names than getNames() reports.
+        mbean.removeAttributeChangeNotificationListener(listener, "A");
+        send(mbean, "A");
+        send(mbean, "B");
+        Assert.assertEquals(List.of("B:hb"), listener.received);
+    }
+
     @Test
     public void testUnknownNameRemovalIsNoOp() throws Exception {
         BaseModelMBean mbean = new BaseModelMBean();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to