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();