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-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 582cde819 EventUtils.EventBindingInvocationHandler.invoke() dispatches 
Object.hashCode/equals/toString into the bound business method 
(empty-eventTypes default), or returns null -> NPE from hash collections 
(non-empty) (f006).
582cde819 is described below

commit 582cde819c6e4e409d87092b77ed4acb79eb53a7
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 07:02:19 2026 -0400

    EventUtils.EventBindingInvocationHandler.invoke() dispatches
    Object.hashCode/equals/toString into the bound business method
    (empty-eventTypes default), or returns null -> NPE from hash collections
    (non-empty) (f006).
---
 src/changes/changes.xml                            |  3 +-
 .../org/apache/commons/lang3/event/EventUtils.java | 15 ++++++++++
 .../apache/commons/lang3/event/EventUtilsTest.java | 34 ++++++++++++++++++++++
 3 files changed, 51 insertions(+), 1 deletion(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 445df41f5..9d960ce8a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -253,7 +253,8 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="gaurav kumar 
pandey, Gary Gregory">Fix DurationFormatUtils.formatPeriod() calculation when 
pattern omits 'M' (#1780).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">FastDateParser parses 'Y' (week year) as plain calendar year; 
asymmetric with FastDatePrinter and with SimpleDateFormat; boundary dates shift 
by a full year, silently (f002).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">UnicodeUnescaper.unescapeJava/EcmaScript/Json: malformed \u sequences 
throw undeclared IllegalArgumentException, AND non-ASCII digit spellings of \u 
escapes are silently accepted; both arms of one missing ASCII-hex prescan 
(f004).</action>
-    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">StringUtils.replaceEachRepeatedly derives its recursion budget from 
the input itself - the documented StackOverflowError protection fails on large 
tables, and expanding rules amplify text 64x even at the default TTL 
(f005).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">StringUtils.replaceEachRepeatedly derives its recursion budget from 
the input itself; the documented StackOverflowError protection fails on large 
tables, and expanding rules amplify text 64x even at the default TTL 
(f005).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">EventUtils.EventBindingInvocationHandler.invoke() dispatches 
Object.hashCode/equals/toString into the bound business method 
(empty-eventTypes default), or returns null -> NPE from hash collections 
(non-empty) (f006).</action>
     <!-- ADD -->
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add JavaVersion.JAVA_27.</action>
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git a/src/main/java/org/apache/commons/lang3/event/EventUtils.java 
b/src/main/java/org/apache/commons/lang3/event/EventUtils.java
index 1ea72d546..8da3b78df 100644
--- a/src/main/java/org/apache/commons/lang3/event/EventUtils.java
+++ b/src/main/java/org/apache/commons/lang3/event/EventUtils.java
@@ -24,6 +24,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.reflect.MethodUtils;
 
 /**
@@ -74,6 +75,20 @@ private boolean hasMatchingParametersMethod(final Method 
method) {
          */
         @Override
         public Object invoke(final Object proxy, final Method method, final 
Object[] parameters) throws Throwable {
+            if (method.getDeclaringClass() == Object.class) {
+                // Handle Object methods locally instead of dispatching them 
to the bound target,
+                // mirroring java.beans.EventHandler: routine host actions 
(hash-based collections,
+                // logging, equality checks during listener de-registration) 
must not invoke the
+                // target method and must not return null into an unboxing 
context.
+                switch (method.getName()) {
+                case "hashCode":
+                    return Integer.valueOf(System.identityHashCode(proxy));
+                case "equals":
+                    return Boolean.valueOf(proxy == parameters[0]);
+                default: // toString
+                    return ObjectUtils.identityToString(proxy);
+                }
+            }
             if (eventTypes.isEmpty() || eventTypes.contains(method.getName())) 
{
                 if (hasMatchingParametersMethod(method)) {
                     return MethodUtils.invokeMethod(target, methodName, 
parameters);
diff --git a/src/test/java/org/apache/commons/lang3/event/EventUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/event/EventUtilsTest.java
index 373ab4412..adaf59851 100644
--- a/src/test/java/org/apache/commons/lang3/event/EventUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/event/EventUtilsTest.java
@@ -112,6 +112,14 @@ public void addMultipleEventListener(final 
MultipleEventListener listener) {
         }
     }
 
+    public static class ListenerCapturingSource {
+        PropertyChangeListener listener;
+
+        public void addPropertyChangeListener(final PropertyChangeListener 
listener) {
+            this.listener = listener;
+        }
+    }
+
     public static class PropertyChangeSource {
         private final EventListenerSupport<PropertyChangeListener> listeners = 
EventListenerSupport.create(PropertyChangeListener.class);
 
@@ -186,6 +194,32 @@ void testBindEventsToMethod() {
         assertEquals(1, counter.getCount());
     }
 
+    @Test
+    void testBindEventsToMethodObjectMethodsNotDispatched() {
+        // Empty eventTypes: every listener-interface method is bound, but 
Object methods must be
+        // handled locally (identity semantics), never dispatched to the bound 
target method.
+        final ListenerCapturingSource src = new ListenerCapturingSource();
+        final EventCounter counter = new EventCounter();
+        EventUtils.bindEventsToMethod(counter, "eventOccurred", src, 
PropertyChangeListener.class);
+        final PropertyChangeListener listener = src.listener;
+        assertEquals(listener.hashCode(), listener.hashCode());
+        assertTrue(listener.equals(listener));
+        assertFalse(listener.equals(counter));
+        assertNotNull(listener.toString());
+        assertEquals(0, counter.getCount(), "Object methods must not invoke 
the bound target method");
+
+        // Non-empty eventTypes: hashCode/equals/toString must not return null 
(previously an NPE
+        // from unboxing inside hash-based collections).
+        final ListenerCapturingSource src2 = new ListenerCapturingSource();
+        final EventCounter counter2 = new EventCounter();
+        EventUtils.bindEventsToMethod(counter2, "eventOccurred", src2, 
PropertyChangeListener.class, "propertyChange");
+        final PropertyChangeListener listener2 = src2.listener;
+        assertEquals(listener2.hashCode(), listener2.hashCode());
+        assertTrue(listener2.equals(listener2));
+        assertNotNull(listener2.toString());
+        assertEquals(0, counter2.getCount());
+    }
+
     @Test
     void testBindEventsToMethodWithEvent() {
         final PropertyChangeSource src = new PropertyChangeSource();

Reply via email to