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

pkarwasz pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/2.x by this push:
     new 49cdf7457e ReadOnlyStringMap: Generalize `equals`/`hashCode` across 
implementations (#3675)
49cdf7457e is described below

commit 49cdf7457eab03df3cc09d9419bb4f40c173dbc8
Author: Ryan Schmitt <[email protected]>
AuthorDate: Mon Jun 2 11:21:28 2025 -0700

    ReadOnlyStringMap: Generalize `equals`/`hashCode` across implementations 
(#3675)
    
    This change rewrites the `equals` and `hashCode` implementations
    throughout the `ReadOnlyStringMap` hierarchy to use a common
    implementation that supports value-based equality between
    implementations.
    
    Fixes #3669.
---
 .../org/apache/log4j/bridge/LogEventWrapper.java   | 19 +++++++++
 .../logging/log4j/spi/DefaultThreadContextMap.java | 16 +++++---
 .../logging/log4j/util/SortedArrayStringMap.java   | 33 ++++------------
 .../apache/logging/log4j/util/package-info.java    |  2 +-
 .../log4j/core/impl/FactoryTestStringMap.java      | 23 +++++++++++
 .../FactoryTestStringMapWithoutIntConstructor.java | 23 +++++++++++
 .../core/impl/JdkMapAdapterStringMapTest.java      | 21 ++++++++++
 .../log4j/core/impl/JdkMapAdapterStringMap.java    | 14 +++++--
 .../logging/log4j/perf/nogc/OpenHashStringMap.java | 46 ++++------------------
 .../3669_generalize_ReadOnlyStringMap_equality.xml | 10 +++++
 10 files changed, 132 insertions(+), 75 deletions(-)

diff --git 
a/log4j-1.2-api/src/main/java/org/apache/log4j/bridge/LogEventWrapper.java 
b/log4j-1.2-api/src/main/java/org/apache/log4j/bridge/LogEventWrapper.java
index 15565aa040..3c5448b210 100644
--- a/log4j-1.2-api/src/main/java/org/apache/log4j/bridge/LogEventWrapper.java
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/bridge/LogEventWrapper.java
@@ -213,5 +213,24 @@ public class LogEventWrapper implements LogEvent {
         public <V> V getValue(final String key) {
             return (V) super.get(key);
         }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            if (obj instanceof ReadOnlyStringMap) {
+                // Convert to maps and compare
+                final Map<String, String> thisMap = toMap();
+                final Map<String, String> otherMap = ((ReadOnlyStringMap) 
obj).toMap();
+                return thisMap.equals(otherMap);
+            }
+            return super.equals(obj);
+        }
+
+        @Override
+        public int hashCode() {
+            return toMap().hashCode();
+        }
     }
 }
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
index f8ea436284..62752428f5 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
@@ -181,21 +181,25 @@ public class DefaultThreadContextMap implements 
ThreadContextMap, ReadOnlyString
 
     @Override
     public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        final Object[] state = localState.get();
-        result = prime * result + ((state == null) ? 0 : 
getMap(state).hashCode());
-        return result;
+        return toMap().hashCode();
     }
 
     @Override
-    public boolean equals(final Object obj) {
+    public boolean equals(Object obj) {
         if (this == obj) {
             return true;
         }
         if (obj == null) {
             return false;
         }
+        if (obj instanceof ReadOnlyStringMap) {
+            if (size() != ((ReadOnlyStringMap) obj).size()) {
+                return false;
+            }
+
+            // Convert to maps and compare
+            obj = ((ReadOnlyStringMap) obj).toMap();
+        }
         if (!(obj instanceof ThreadContextMap)) {
             return false;
         }
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/util/SortedArrayStringMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/SortedArrayStringMap.java
index ac6b3ed199..f74b10fbf2 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/util/SortedArrayStringMap.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/SortedArrayStringMap.java
@@ -408,39 +408,22 @@ public class SortedArrayStringMap implements 
IndexedStringMap {
         if (obj == this) {
             return true;
         }
-        if (!(obj instanceof SortedArrayStringMap)) {
+        if (!(obj instanceof ReadOnlyStringMap)) {
             return false;
         }
-        final SortedArrayStringMap other = (SortedArrayStringMap) obj;
-        if (this.size() != other.size()) {
+        if (size() != ((ReadOnlyStringMap) obj).size()) {
             return false;
         }
-        for (int i = 0; i < size(); i++) {
-            if (!Objects.equals(keys[i], other.keys[i])) {
-                return false;
-            }
-            if (!Objects.equals(values[i], other.values[i])) {
-                return false;
-            }
-        }
-        return true;
+
+        // Convert to maps and compare
+        final Map<String, String> thisMap = toMap();
+        final Map<String, String> otherMap = ((ReadOnlyStringMap) obj).toMap();
+        return thisMap.equals(otherMap);
     }
 
     @Override
     public int hashCode() {
-        int result = 37;
-        result = HASHVAL * result + size;
-        result = HASHVAL * result + hashCode(keys, size);
-        result = HASHVAL * result + hashCode(values, size);
-        return result;
-    }
-
-    private static int hashCode(final Object[] values, final int length) {
-        int result = 1;
-        for (int i = 0; i < length; i++) {
-            result = HASHVAL * result + (values[i] == null ? 0 : 
values[i].hashCode());
-        }
-        return result;
+        return toMap().hashCode();
     }
 
     @Override
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/util/package-info.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/package-info.java
index ea9e726d42..23fcfbd931 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/package-info.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/package-info.java
@@ -20,7 +20,7 @@
  * There are no guarantees for binary or logical compatibility in this package.
  */
 @Export
-@Version("2.24.1")
+@Version("2.25.0")
 package org.apache.logging.log4j.util;
 
 import org.osgi.annotation.bundle.Export;
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/FactoryTestStringMap.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/FactoryTestStringMap.java
index 3c9b2dce9a..8a9ec1bfa0 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/FactoryTestStringMap.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/FactoryTestStringMap.java
@@ -114,4 +114,27 @@ public class FactoryTestStringMap implements 
IndexedStringMap {
     public Map<String, String> toMap() {
         return null;
     }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof ReadOnlyStringMap)) {
+            return false;
+        }
+        if (size() != ((ReadOnlyStringMap) obj).size()) {
+            return false;
+        }
+
+        // Convert to maps and compare
+        final Map<String, String> thisMap = toMap();
+        final Map<String, String> otherMap = ((ReadOnlyStringMap) obj).toMap();
+        return thisMap.equals(otherMap);
+    }
+
+    @Override
+    public int hashCode() {
+        return toMap().hashCode();
+    }
 }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/FactoryTestStringMapWithoutIntConstructor.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/FactoryTestStringMapWithoutIntConstructor.java
index 6359224090..74947ba75f 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/FactoryTestStringMapWithoutIntConstructor.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/FactoryTestStringMapWithoutIntConstructor.java
@@ -81,4 +81,27 @@ public class FactoryTestStringMapWithoutIntConstructor 
implements StringMap {
 
     @Override
     public void remove(final String key) {}
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof ReadOnlyStringMap)) {
+            return false;
+        }
+        if (size() != ((ReadOnlyStringMap) obj).size()) {
+            return false;
+        }
+
+        // Convert to maps and compare
+        final Map<String, String> thisMap = toMap();
+        final Map<String, String> otherMap = ((ReadOnlyStringMap) obj).toMap();
+        return thisMap.equals(otherMap);
+    }
+
+    @Override
+    public int hashCode() {
+        return toMap().hashCode();
+    }
 }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMapTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMapTest.java
index 7b06a23a32..f9c5ab3a40 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMapTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMapTest.java
@@ -35,6 +35,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.stream.Stream;
 import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.TriConsumer;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -850,6 +851,26 @@ class JdkMapAdapterStringMapTest {
         assertEquals(state.count, original.size());
     }
 
+    @Test
+    void testEqualityWithOtherImplementations() {
+        final JdkMapAdapterStringMap left = new JdkMapAdapterStringMap();
+        final SortedArrayStringMap right = new SortedArrayStringMap();
+        assertEquals(left, right);
+        assertEquals(left.hashCode(), right.hashCode());
+
+        left.putValue("a", "avalue");
+        left.putValue("B", "Bvalue");
+        right.putValue("B", "Bvalue");
+        right.putValue("a", "avalue");
+        assertEquals(left, right);
+        assertEquals(left.hashCode(), right.hashCode());
+
+        left.remove("a");
+        right.remove("a");
+        assertEquals(left, right);
+        assertEquals(left.hashCode(), right.hashCode());
+    }
+
     static Stream<Arguments> testImmutability() {
         return Stream.of(
                 Arguments.of(new HashMap<>(), false),
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
index 09c4441394..d4833bb40c 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
@@ -219,15 +219,21 @@ public class JdkMapAdapterStringMap implements StringMap {
         if (object == this) {
             return true;
         }
-        if (!(object instanceof JdkMapAdapterStringMap)) {
+        if (!(object instanceof ReadOnlyStringMap)) {
             return false;
         }
-        final JdkMapAdapterStringMap other = (JdkMapAdapterStringMap) object;
-        return map.equals(other.map) && immutable == other.immutable;
+        if (size() != ((ReadOnlyStringMap) object).size()) {
+            return false;
+        }
+
+        // Convert to maps and compare
+        final Map<String, String> thisMap = toMap();
+        final Map<String, String> otherMap = ((ReadOnlyStringMap) 
object).toMap();
+        return thisMap.equals(otherMap);
     }
 
     @Override
     public int hashCode() {
-        return map.hashCode() + (immutable ? 31 : 0);
+        return toMap().hashCode();
     }
 }
diff --git 
a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java
 
b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java
index 2e26a3b152..5e1f3f4fc5 100644
--- 
a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java
+++ 
b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java
@@ -24,7 +24,6 @@ import java.util.Collections;
 import java.util.ConcurrentModificationException;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Objects;
 import org.apache.logging.log4j.spi.ThreadContextMap;
 import org.apache.logging.log4j.util.BiConsumer;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
@@ -301,27 +300,14 @@ public class OpenHashStringMap<K, V> implements 
StringMap, ThreadContextMap {
         if (!(obj instanceof ReadOnlyStringMap)) {
             return false;
         }
-        final ReadOnlyStringMap other = (ReadOnlyStringMap) obj;
-        if (other.size() != size()) {
+        if (size() != ((ReadOnlyStringMap) obj).size()) {
             return false;
         }
-        int pos = arraySize;
-        if (containsNullKey) {
-            if (!Objects.equals(getObjectValue(null), other.getValue(null))) {
-                return false;
-            }
-        }
-        --pos;
-        final K myKeys[] = this.keys;
-        for (; pos >= 0; pos--) {
-            K k;
-            if ((k = myKeys[pos]) != null) {
-                if (!Objects.equals(values[pos], other.getValue((String) k))) {
-                    return false;
-                }
-            }
-        }
-        return true;
+
+        // Convert to maps and compare
+        final Map<String, String> thisMap = toMap();
+        final Map<String, String> otherMap = ((ReadOnlyStringMap) obj).toMap();
+        return thisMap.equals(otherMap);
     }
 
     @Override
@@ -699,25 +685,7 @@ public class OpenHashStringMap<K, V> implements StringMap, 
ThreadContextMap {
      */
     @Override
     public int hashCode() {
-        int result = 0;
-        for (int j = realSize(), i = 0, t = 0; j-- != 0; ) {
-            while (keys[i] == null) {
-                i++;
-            }
-            if (this != keys[i]) {
-                t = keys[i].hashCode();
-            }
-            if (this != values[i]) {
-                t ^= (values[i] == null ? 0 : values[i].hashCode());
-            }
-            result += t;
-            i++;
-        }
-        // Zero / null keys have hash zero.
-        if (containsNullKey) {
-            result += (values[arraySize] == null ? 0 : 
values[arraySize].hashCode());
-        }
-        return result;
+        return toMap().hashCode();
     }
 
     @SuppressWarnings("unchecked")
diff --git 
a/src/changelog/.2.x.x/3669_generalize_ReadOnlyStringMap_equality.xml 
b/src/changelog/.2.x.x/3669_generalize_ReadOnlyStringMap_equality.xml
new file mode 100644
index 0000000000..a7a6ee4a08
--- /dev/null
+++ b/src/changelog/.2.x.x/3669_generalize_ReadOnlyStringMap_equality.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xmlns="https://logging.apache.org/xml/ns";
+       xsi:schemaLocation="https://logging.apache.org/xml/ns 
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="fixed">
+  <issue id="3669" 
link="https://github.com/apache/logging-log4j2/issues/3669"/>
+  <description format="asciidoc">
+    The ReadOnlyStringMap implementations now support equality comparisons 
against each other.
+  </description>
+</entry>

Reply via email to