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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 0294443cc256 CAMEL-25059: camel-util - CaseInsensitiveMap keySet() 
removes keys case-insensitively (#26942)
0294443cc256 is described below

commit 0294443cc25661d7aef73f3fdab5423aff9ef42e
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 2 19:49:30 2026 +0530

    CAMEL-25059: camel-util - CaseInsensitiveMap keySet() removes keys 
case-insensitively (#26942)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/impl/DefaultMessageHeaderTest.java       | 23 +++++
 .../apache/camel/util/CaseInsensitiveMapTest.java  | 72 ++++++++++++++++
 .../org/apache/camel/util/CaseInsensitiveMap.java  | 97 +++++++++++++++++++++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  8 ++
 4 files changed, 199 insertions(+), 1 deletion(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultMessageHeaderTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultMessageHeaderTest.java
index ac71891e43e9..f192b3dd4a0b 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultMessageHeaderTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultMessageHeaderTest.java
@@ -719,6 +719,29 @@ public class DefaultMessageHeaderTest {
         assertEquals(1, copy.getHeaders().size());
     }
 
+    @Test
+    public void testCopyOnWriteKeySetRemoveWithDifferentCase() {
+        DefaultMessage original = new DefaultMessage(camelContext);
+        original.setHeader("X-Trace", "abc");
+        original.setHeader("X-Tenant", "t1");
+        original.setHeader("Accept", "text/plain");
+
+        DefaultMessage copy = new DefaultMessage(camelContext);
+        copy.copyFrom(original);
+
+        assertTrue(copy.getHeaders().keySet().remove("x-trace"));
+        assertEquals(Set.of("X-Tenant", "Accept"), 
Set.copyOf(copy.getHeaders().keySet()));
+
+        assertTrue(copy.getHeaders().keySet().removeAll(Set.of("x-tenant")));
+        assertEquals(Set.of("Accept"), Set.copyOf(copy.getHeaders().keySet()));
+
+        // original unaffected
+        assertEquals(Set.of("X-Trace", "X-Tenant", "Accept"), 
Set.copyOf(original.getHeaders().keySet()));
+
+        assertTrue(original.getHeaders().keySet().retainAll(Set.of("accept")));
+        assertEquals(Set.of("Accept"), 
Set.copyOf(original.getHeaders().keySet()));
+    }
+
     @Test
     public void testLazyCopyOnWriteValues() {
         DefaultMessage original = new DefaultMessage(camelContext);
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java
index 45f430d564de..c8ee0d385dad 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/util/CaseInsensitiveMapTest.java
@@ -256,6 +256,78 @@ public class CaseInsensitiveMapTest {
         assertTrue(keys.contains("BAZ"));
     }
 
+    @Test
+    public void testKeySetRemoveWithDifferentCase() {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("X-Trace", "abc");
+        map.put("Accept", "text/plain");
+
+        assertTrue(map.keySet().remove("x-trace"));
+        assertFalse(map.containsKey("X-Trace"));
+        assertEquals(List.of("Accept"), new ArrayList<>(map.keySet()));
+
+        assertFalse(map.keySet().remove("x-trace"));
+        assertFalse(map.keySet().remove("x-other"));
+        assertEquals(1, map.size());
+    }
+
+    @Test
+    public void testKeySetRemoveAllWithDifferentCase() {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("X-Trace", "abc");
+        map.put("X-TENANT", "t1");
+        map.put("Accept", "text/plain");
+
+        // fewer elements than the map
+        assertTrue(map.keySet().removeAll(List.of("x-trace", "x-tenant")));
+        assertEquals(List.of("Accept"), new ArrayList<>(map.keySet()));
+        assertFalse(map.keySet().removeAll(List.of("x-trace", "x-tenant")));
+
+        // more elements than the map: the result must not depend on the sizes
+        map.put("X-Trace", "abc");
+        map.put("X-TENANT", "t1");
+        assertTrue(map.keySet().removeAll(List.of("x-trace", "x-tenant", 
"x-other", "x-more")));
+        assertEquals(List.of("Accept"), new ArrayList<>(map.keySet()));
+        assertEquals("text/plain", map.get("accept"));
+    }
+
+    @Test
+    public void testKeySetRetainAllWithDifferentCase() {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("X-Trace", "abc");
+        map.put("X-TENANT", "t1");
+        map.put("Accept", "text/plain");
+
+        assertTrue(map.keySet().retainAll(List.of("accept", "x-tenant", 
"x-other")));
+        assertEquals(List.of("X-TENANT", "Accept"), new 
ArrayList<>(map.keySet()));
+        assertFalse(map.keySet().retainAll(Set.of("ACCEPT", "X-Tenant")));
+        assertEquals(2, map.size());
+
+        assertTrue(map.keySet().retainAll(List.of()));
+        assertTrue(map.isEmpty());
+    }
+
+    @Test
+    public void testKeySetIteratorRemove() {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("X-Trace", "abc");
+        map.put("X-Tenant", "t1");
+        map.put("Accept", "text/plain");
+
+        Iterator<String> it = map.keySet().iterator();
+        assertEquals("X-Trace", it.next());
+        assertEquals("X-Tenant", it.next());
+        it.remove();
+        assertThrows(IllegalStateException.class, it::remove);
+        assertEquals("Accept", it.next());
+        assertFalse(it.hasNext());
+
+        assertEquals(List.of("X-Trace", "Accept"), new 
ArrayList<>(map.keySet()));
+
+        assertTrue(map.keySet().removeIf(k -> k.startsWith("X-")));
+        assertEquals(List.of("Accept"), new ArrayList<>(map.keySet()));
+    }
+
     @Test
     public void testRetainKeysCopyToAnotherMap() {
         Map<String, Object> map = new CaseInsensitiveMap();
diff --git 
a/core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java 
b/core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java
index dda61c26fb09..38e2a58e3cf2 100644
--- 
a/core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java
+++ 
b/core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java
@@ -335,6 +335,97 @@ public class CaseInsensitiveMap extends 
AbstractMap<String, Object> implements S
         return new EntrySet();
     }
 
+    /**
+     * Returns a view of the keys, which keep their original case. Like the 
lookups of this map, {@code contains},
+     * {@code remove}, {@code removeAll} and {@code retainAll} compare the 
keys case-insensitively.
+     */
+    @Override
+    public Set<String> keySet() {
+        return new KeySet();
+    }
+
+    private final class KeySet extends AbstractSet<String> {
+        @Override
+        public int size() {
+            return size;
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return containsKey(o);
+        }
+
+        @Override
+        public boolean remove(Object o) {
+            int idx = findIndex((String) o);
+            if (idx == EMPTY) {
+                return false;
+            }
+            removeByIndex(idx);
+            return true;
+        }
+
+        @Override
+        public boolean removeAll(Collection<?> c) {
+            // remove each element by key, so the result does not depend on 
the sizes
+            // (AbstractSet.removeAll would use c.contains(key), which is 
case-sensitive for most collections)
+            Objects.requireNonNull(c);
+            boolean modified = false;
+            for (Object o : c) {
+                modified |= remove(o);
+            }
+            return modified;
+        }
+
+        @Override
+        public boolean retainAll(Collection<?> c) {
+            Objects.requireNonNull(c);
+            CaseInsensitiveMap retain = new CaseInsensitiveMap();
+            for (Object o : c) {
+                if (o instanceof String key) {
+                    retain.put(key, Boolean.TRUE);
+                }
+            }
+            boolean modified = false;
+            for (int i = 0; i < usedSlots; i++) {
+                if (keys[i] != null && !retain.containsKey(keys[i])) {
+                    removeByIndex(i);
+                    modified = true;
+                }
+            }
+            return modified;
+        }
+
+        @Override
+        public void clear() {
+            CaseInsensitiveMap.this.clear();
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return new KeyIterator();
+        }
+    }
+
+    private final class KeyIterator implements Iterator<String> {
+        private final EntryIterator it = new EntryIterator();
+
+        @Override
+        public boolean hasNext() {
+            return it.hasNext();
+        }
+
+        @Override
+        public String next() {
+            return keys[it.nextIndex()];
+        }
+
+        @Override
+        public void remove() {
+            it.remove();
+        }
+    }
+
     private final class EntrySet extends AbstractSet<Entry<String, Object>> {
         @Override
         public int size() {
@@ -398,12 +489,16 @@ public class CaseInsensitiveMap extends 
AbstractMap<String, Object> implements S
 
         @Override
         public Entry<String, Object> next() {
+            return new MapEntry(nextIndex());
+        }
+
+        int nextIndex() {
             if (cursor == EMPTY) {
                 throw new NoSuchElementException();
             }
             lastReturned = cursor;
             cursor = advance(cursor + 1);
-            return new MapEntry(lastReturned);
+            return lastReturned;
         }
 
         @Override
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 39c68ea84b31..1a46cac9d9da 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -1261,6 +1261,14 @@ The message of the part is copied into the child 
exchange, so the exchanges in t
 untouched and remain usable after the split, for example by the batching 
consumer that owns them. Exchange
 properties of the part are not carried over to the child exchange, as is 
already the case for `Message` parts.
 
+=== camel-util - retainAll on the key set of the message headers ignores case
+
+`retainAll` on the key set of `CaseInsensitiveMap`, and so on 
`exchange.getMessage().getHeaders().keySet()`, now
+keeps a key when the given collection holds its name in any case, as lookups 
do. For example
+`headers.keySet().retainAll(List.of("accept"))` now keeps the `Accept` header. 
Prior to Camel 4.23 the names were
+compared case-sensitively, and such a call removed `Accept`. To keep headers 
by their exact name, remove the others
+explicitly, for example with `headers.keySet().removeIf(name -> 
!names.contains(name))`.
+
 === camel-core - Idempotent Consumer no longer stops its repository when the 
route is stopped
 
 Stopping a route with an Idempotent Consumer (for example with `stopRoute` on 
the route controller, over JMX,

Reply via email to