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,