This is an automated email from the ASF dual-hosted git repository.
gnodet 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 da023d6b121a CAMEL-23691: Improve CaseInsensitiveMap with O(1) hash
table and header key deduplication
da023d6b121a is described below
commit da023d6b121a6aca3b24271617d78161851af227
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Jun 5 00:05:28 2026 +0200
CAMEL-23691: Improve CaseInsensitiveMap with O(1) hash table and header key
deduplication
Replace the TreeMap-based CaseInsensitiveMap with a custom hash table:
- O(1) get/put/containsKey/remove (was O(log n))
- Zero-allocation lookups with char-by-char case-insensitive hashing
- Header key deduplication via ExchangeConstantProvider (151 known keys)
- Insertion-order iteration, no external dependencies, no ThreadLocal
- Eliminates the need for camel-headersmap in most cases
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../org/apache/camel/ExchangeConstantProvider.java | 8 +-
.../impl/engine/DefaultHeadersMapFactory.java | 5 +
.../apache/camel/util/CaseInsensitiveMapTest.java | 188 +++++++++
.../org/apache/camel/util/CaseInsensitiveMap.java | 418 ++++++++++++++++++++-
.../main/resources/velocity/constant-provider.vm | 8 +-
5 files changed, 618 insertions(+), 9 deletions(-)
diff --git
a/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java
b/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java
index f1dcc4d60bd6..b68d1400ffd4 100644
---
a/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java
+++
b/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java
@@ -1,10 +1,12 @@
/* Generated by camel build tools - do NOT edit this file! */
package org.apache.camel;
-import javax.annotation.processing.Generated;
+import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
+import javax.annotation.processing.Generated;
+
import org.jspecify.annotations.Nullable;
/**
@@ -173,5 +175,9 @@ public class ExchangeConstantProvider {
public static @Nullable String lookup(String key) {
return MAP.get(key);
}
+
+ public static Collection<String> values() {
+ return MAP.values();
+ }
}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultHeadersMapFactory.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultHeadersMapFactory.java
index adb863b99900..bbbb20b1fcb9 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultHeadersMapFactory.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultHeadersMapFactory.java
@@ -18,6 +18,7 @@ package org.apache.camel.impl.engine;
import java.util.Map;
+import org.apache.camel.ExchangeConstantProvider;
import org.apache.camel.spi.HeadersMapFactory;
import org.apache.camel.util.CaseInsensitiveMap;
@@ -29,6 +30,10 @@ import org.apache.camel.util.CaseInsensitiveMap;
*/
public class DefaultHeadersMapFactory implements HeadersMapFactory {
+ static {
+
CaseInsensitiveMap.registerKnownKeys(ExchangeConstantProvider.values());
+ }
+
@Override
public Map<String, Object> newMap() {
return new CaseInsensitiveMap();
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 71768207b71b..b5edbdddeba1 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
@@ -502,6 +502,194 @@ public class CaseInsensitiveMapTest {
service.shutdownNow();
}
+ @Test
+ public void testInsertionOrder() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("Zebra", 1);
+ map.put("apple", 2);
+ map.put("Mango", 3);
+
+ Iterator<Map.Entry<String, Object>> it = map.entrySet().iterator();
+ Map.Entry<String, Object> e1 = it.next();
+ Map.Entry<String, Object> e2 = it.next();
+ Map.Entry<String, Object> e3 = it.next();
+ assertFalse(it.hasNext());
+
+ assertEquals("Zebra", e1.getKey());
+ assertEquals(1, e1.getValue());
+ assertEquals("apple", e2.getKey());
+ assertEquals(2, e2.getValue());
+ assertEquals("Mango", e3.getKey());
+ assertEquals(3, e3.getValue());
+ }
+
+ @Test
+ public void testIteratorRemove() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("Foo", "cheese");
+ map.put("Bar", "cake");
+ map.put("Baz", "beer");
+
+ Iterator<Map.Entry<String, Object>> it = map.entrySet().iterator();
+ it.next(); // Foo
+ Map.Entry<String, Object> bar = it.next();
+ assertEquals("Bar", bar.getKey());
+ it.remove();
+
+ assertEquals(2, map.size());
+ assertFalse(map.containsKey("Bar"));
+ assertTrue(map.containsKey("Foo"));
+ assertTrue(map.containsKey("Baz"));
+ }
+
+ @Test
+ public void testEntrySetRemove() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("Foo", "cheese");
+ map.put("Bar", "cake");
+
+ // case-insensitive key match + value match → removes
+ boolean removed = map.entrySet().remove(Map.entry("foo", "cheese"));
+ assertTrue(removed);
+ assertEquals(1, map.size());
+ assertFalse(map.containsKey("Foo"));
+
+ // wrong value → does not remove
+ boolean notRemoved = map.entrySet().remove(Map.entry("bar", "wrong"));
+ assertFalse(notRemoved);
+ assertEquals(1, map.size());
+ }
+
+ @Test
+ public void testEntrySetValue() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("Foo", "cheese");
+
+ Map.Entry<String, Object> entry = map.entrySet().iterator().next();
+ assertEquals("cheese", entry.getValue());
+
+ Object old = entry.setValue("cake");
+ assertEquals("cheese", old);
+ assertEquals("cake", entry.getValue());
+ assertEquals("cake", map.get("Foo"));
+ }
+
+ @Test
+ public void testRemoveThenReput() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("Foo", "cheese");
+ map.remove("foo");
+ assertTrue(map.isEmpty());
+
+ map.put("FOO", "cake");
+ assertEquals(1, map.size());
+ assertEquals("cake", map.get("foo"));
+
+ // new key case should be used since old entry was removed
+ Map<String, Object> copy = new HashMap<>(map);
+ assertTrue(copy.containsKey("FOO"));
+ assertFalse(copy.containsKey("Foo"));
+ }
+
+ @Test
+ public void testResize() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ for (int i = 0; i < 200; i++) {
+ map.put("key" + i, i);
+ }
+ assertEquals(200, map.size());
+
+ for (int i = 0; i < 200; i++) {
+ assertTrue(map.containsKey("KEY" + i));
+ assertEquals(i, map.get("key" + i));
+ }
+
+ // remove half and verify
+ for (int i = 0; i < 100; i++) {
+ map.remove("Key" + i);
+ }
+ assertEquals(100, map.size());
+
+ for (int i = 100; i < 200; i++) {
+ assertEquals(i, map.get("KEY" + i));
+ }
+ }
+
+ @Test
+ public void testContainsValue() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("foo", "cheese");
+ map.put("bar", null);
+
+ assertTrue(map.containsValue("cheese"));
+ assertTrue(map.containsValue(null));
+ assertFalse(map.containsValue("missing"));
+ }
+
+ @Test
+ public void testNullValue() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("foo", null);
+
+ assertEquals(1, map.size());
+ assertTrue(map.containsKey("FOO"));
+ assertNull(map.get("foo"));
+
+ // distinguish null value from missing key
+ assertTrue(map.containsKey("foo"));
+ assertFalse(map.containsKey("bar"));
+ }
+
+ @Test
+ public void testClearAndReuse() {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("Foo", "cheese");
+ map.put("Bar", "cake");
+ assertEquals(2, map.size());
+
+ map.clear();
+ assertEquals(0, map.size());
+ assertTrue(map.isEmpty());
+ assertFalse(map.containsKey("Foo"));
+
+ // reuse after clear
+ map.put("Baz", "beer");
+ assertEquals(1, map.size());
+ assertEquals("beer", map.get("BAZ"));
+ }
+
+ @Test
+ public void testKnownKeyDeduplication() {
+ // Register known keys
+ CaseInsensitiveMap.registerKnownKeys(List.of("CamelCharsetName",
"CamelExchangeId", "breadcrumbId"));
+
+ Map<String, Object> map = new CaseInsensitiveMap();
+
+ // simulate deserialized key (new String to guarantee a different
object)
+ String deserializedKey = new String("CamelCharsetName");
+ map.put(deserializedKey, "UTF-8");
+
+ // the stored key should be the canonical reference, not the
deserialized copy
+ Map.Entry<String, Object> entry = map.entrySet().iterator().next();
+ assertSame("CamelCharsetName", entry.getKey());
+ assertNotSame(deserializedKey, entry.getKey());
+
+ // case-insensitive dedup: different case should still map to canonical
+ Map<String, Object> map2 = new CaseInsensitiveMap();
+ map2.put("camelcharsetname", "UTF-8");
+ Map.Entry<String, Object> entry2 = map2.entrySet().iterator().next();
+ assertSame("CamelCharsetName", entry2.getKey());
+
+ // non-registered key is stored as-is
+ String custom = new String("CustomHeader");
+ map.put(custom, "value");
+ for (Map.Entry<String, Object> e : map.entrySet()) {
+ if (e.getValue().equals("value")) {
+ assertSame(custom, e.getKey());
+ }
+ }
+ }
+
@Disabled("Manual test")
@Test
public void testCopyMapWithCamelHeadersTest() throws Exception {
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 92155488e967..7f0cbe5f35e2 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
@@ -16,31 +16,435 @@
*/
package org.apache.camel.util;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.io.Serial;
+import java.io.Serializable;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
import java.util.Map;
-import java.util.TreeMap;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
/**
* A map that uses case insensitive keys, but preserves the original key cases.
* <p/>
- * The map is based on {@link TreeMap} and therefore uses O(n) for lookup and
not O(1) as a {@link java.util.HashMap}
- * does.
+ * The map uses a custom hash table with case-insensitive hashing and
comparison, providing O(1) for {@code get},
+ * {@code put}, {@code containsKey} and {@code remove} operations without
allocating temporary strings. Entries are
+ * stored in insertion order.
* <p/>
* This map is <b>not</b> designed to be thread safe as concurrent access to
it is not supposed to be performed by the
* Camel routing engine.
*/
-public class CaseInsensitiveMap extends TreeMap<String, Object> {
+public class CaseInsensitiveMap extends AbstractMap<String, Object> implements
Serializable {
private static final @Serial long serialVersionUID = -8538318195477618308L;
+ private static final int DEFAULT_CAPACITY = 16;
+ private static final float LOAD_FACTOR = 0.75f;
+ private static final int EMPTY = -1;
+
+ // Static lookup table for deduplicating well-known header keys (e.g.
Exchange constants).
+ // Registered once at startup; read-only after that. Zero-allocation
lookups.
+ private static volatile int[] knownTable;
+ private static volatile String[] knownEntries;
+ private static volatile int[] knownChainNext;
+ private static volatile int knownMask;
+
+ /**
+ * Registers a set of well-known header key strings for deduplication.
When a key passed to {@link #put} matches one
+ * of these strings (case-insensitive), the canonical reference from this
set is stored instead of the caller's
+ * string, reducing memory when many map instances carry the same headers
(e.g. deserialized exchanges).
+ * <p/>
+ * This method is intended to be called once during framework startup.
+ */
+ public static void registerKnownKeys(Collection<String> keys) {
+ int sz = keys.size();
+ int tableSize = tableSizeFor(Math.max((int) (sz / LOAD_FACTOR) + 1,
DEFAULT_CAPACITY));
+ int mask = tableSize - 1;
+ int[] tbl = new int[tableSize];
+ Arrays.fill(tbl, EMPTY);
+ String[] entries = keys.toArray(new String[0]);
+ int[] chain = new int[entries.length];
+
+ for (int i = 0; i < entries.length; i++) {
+ int b = caseInsensitiveHash(entries[i]) & mask;
+ chain[i] = tbl[b];
+ tbl[b] = i;
+ }
+
+ knownEntries = entries;
+ knownChainNext = chain;
+ knownMask = mask;
+ // assign table last — readers check knownTable != null as the gate
+ knownTable = tbl;
+ }
+
+ private static String deduplicateKey(String key) {
+ int[] tbl = knownTable;
+ if (tbl == null) {
+ return key;
+ }
+ int idx = tbl[caseInsensitiveHash(key) & knownMask];
+ while (idx != EMPTY) {
+ if (knownEntries[idx].equalsIgnoreCase(key)) {
+ return knownEntries[idx];
+ }
+ idx = knownChainNext[idx];
+ }
+ return key;
+ }
+
+ private transient int[] table;
+ private transient String[] keys;
+ private transient Object[] values;
+ private transient int[] chainNext;
+
+ private transient int size;
+ private transient int usedSlots;
+ private transient int threshold;
public CaseInsensitiveMap() {
- super(String.CASE_INSENSITIVE_ORDER);
+ init(DEFAULT_CAPACITY);
}
public CaseInsensitiveMap(Map<? extends String, ?> map) {
- // must use the insensitive order
- super(String.CASE_INSENSITIVE_ORDER);
+ init(tableSizeFor(Math.max((int) (map.size() / LOAD_FACTOR) + 1,
DEFAULT_CAPACITY)));
putAll(map);
}
+ private void init(int tableCapacity) {
+ table = new int[tableCapacity];
+ Arrays.fill(table, EMPTY);
+ int entryCapacity = (int) (tableCapacity * LOAD_FACTOR) + 1;
+ keys = new String[entryCapacity];
+ values = new Object[entryCapacity];
+ chainNext = new int[entryCapacity];
+ size = 0;
+ usedSlots = 0;
+ threshold = (int) (tableCapacity * LOAD_FACTOR);
+ }
+
+ private static int tableSizeFor(int cap) {
+ int n = cap - 1;
+ n |= n >>> 1;
+ n |= n >>> 2;
+ n |= n >>> 4;
+ n |= n >>> 8;
+ n |= n >>> 16;
+ return Math.max(DEFAULT_CAPACITY, n + 1);
+ }
+
+ static int caseInsensitiveHash(String key) {
+ int h = 0;
+ for (int i = 0, len = key.length(); i < len; i++) {
+ // two-step fold matches String.equalsIgnoreCase /
CASE_INSENSITIVE_ORDER
+ h = 31 * h +
Character.toLowerCase(Character.toUpperCase(key.charAt(i)));
+ }
+ return h ^ (h >>> 16);
+ }
+
+ private int bucketIndex(String key) {
+ return caseInsensitiveHash(key) & (table.length - 1);
+ }
+
+ private int findIndex(String key) {
+ int idx = table[bucketIndex(key)];
+ while (idx != EMPTY) {
+ if (keys[idx].equalsIgnoreCase(key)) {
+ return idx;
+ }
+ idx = chainNext[idx];
+ }
+ return EMPTY;
+ }
+
+ @Override
+ public Object get(Object key) {
+ int idx = findIndex((String) key);
+ return idx != EMPTY ? values[idx] : null;
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return findIndex((String) key) != EMPTY;
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ for (int i = 0; i < usedSlots; i++) {
+ if (keys[i] != null && Objects.equals(value, values[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Object put(String key, Object value) {
+ key = deduplicateKey(key);
+ int idx = findIndex(key);
+ if (idx != EMPTY) {
+ Object old = values[idx];
+ values[idx] = value;
+ return old;
+ }
+ if (size >= threshold) {
+ resize(table.length * 2);
+ }
+ if (usedSlots >= keys.length) {
+ int newCap = keys.length + (keys.length >> 1);
+ keys = Arrays.copyOf(keys, newCap);
+ values = Arrays.copyOf(values, newCap);
+ chainNext = Arrays.copyOf(chainNext, newCap);
+ }
+ int slot = usedSlots++;
+ keys[slot] = key;
+ values[slot] = value;
+ int b = bucketIndex(key);
+ chainNext[slot] = table[b];
+ table[b] = slot;
+ size++;
+ return null;
+ }
+
+ @Override
+ public Object remove(Object key) {
+ int idx = findIndex((String) key);
+ if (idx != EMPTY) {
+ Object old = values[idx];
+ removeByIndex(idx);
+ return old;
+ }
+ return null;
+ }
+
+ private void removeByIndex(int idx) {
+ String key = keys[idx];
+ int b = bucketIndex(key);
+ int prev = EMPTY;
+ int cur = table[b];
+ while (cur != EMPTY) {
+ if (cur == idx) {
+ if (prev == EMPTY) {
+ table[b] = chainNext[idx];
+ } else {
+ chainNext[prev] = chainNext[idx];
+ }
+ break;
+ }
+ prev = cur;
+ cur = chainNext[cur];
+ }
+ keys[idx] = null;
+ values[idx] = null;
+ size--;
+ }
+
+ private void resize(int newTableCapacity) {
+ int[] newTable = new int[newTableCapacity];
+ Arrays.fill(newTable, EMPTY);
+ int entryCap = Math.max((int) (newTableCapacity * LOAD_FACTOR) + 1,
size + 1);
+ String[] newKeys = new String[entryCap];
+ Object[] newValues = new Object[entryCap];
+ int[] newChainNext = new int[entryCap];
+
+ int newSlot = 0;
+ for (int i = 0; i < usedSlots; i++) {
+ if (keys[i] != null) {
+ newKeys[newSlot] = keys[i];
+ newValues[newSlot] = values[i];
+ int b = caseInsensitiveHash(keys[i]) & (newTableCapacity - 1);
+ newChainNext[newSlot] = newTable[b];
+ newTable[b] = newSlot;
+ newSlot++;
+ }
+ }
+
+ table = newTable;
+ keys = newKeys;
+ values = newValues;
+ chainNext = newChainNext;
+ usedSlots = newSlot;
+ threshold = (int) (newTableCapacity * LOAD_FACTOR);
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return size == 0;
+ }
+
+ @Override
+ public void clear() {
+ Arrays.fill(table, EMPTY);
+ Arrays.fill(keys, 0, usedSlots, null);
+ Arrays.fill(values, 0, usedSlots, null);
+ size = 0;
+ usedSlots = 0;
+ }
+
+ @Override
+ public void putAll(Map<? extends String, ?> m) {
+ for (Entry<? extends String, ?> entry : m.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public Set<Entry<String, Object>> entrySet() {
+ return new EntrySet();
+ }
+
+ private final class EntrySet extends AbstractSet<Entry<String, Object>> {
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ if (!(o instanceof Entry<?, ?> e)) {
+ return false;
+ }
+ int idx = findIndex((String) e.getKey());
+ return idx != EMPTY && Objects.equals(values[idx], e.getValue());
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ if (!(o instanceof Entry<?, ?> e)) {
+ return false;
+ }
+ int idx = findIndex((String) e.getKey());
+ if (idx != EMPTY && Objects.equals(values[idx], e.getValue())) {
+ removeByIndex(idx);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ CaseInsensitiveMap.this.clear();
+ }
+
+ @Override
+ public Iterator<Entry<String, Object>> iterator() {
+ return new EntryIterator();
+ }
+ }
+
+ private final class EntryIterator implements Iterator<Entry<String,
Object>> {
+ private int cursor;
+ private int lastReturned = EMPTY;
+
+ EntryIterator() {
+ cursor = advance(0);
+ }
+
+ private int advance(int from) {
+ for (int i = from; i < usedSlots; i++) {
+ if (keys[i] != null) {
+ return i;
+ }
+ }
+ return EMPTY;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return cursor != EMPTY;
+ }
+
+ @Override
+ public Entry<String, Object> next() {
+ if (cursor == EMPTY) {
+ throw new NoSuchElementException();
+ }
+ lastReturned = cursor;
+ cursor = advance(cursor + 1);
+ return new MapEntry(lastReturned);
+ }
+
+ @Override
+ public void remove() {
+ if (lastReturned == EMPTY) {
+ throw new IllegalStateException();
+ }
+ removeByIndex(lastReturned);
+ lastReturned = EMPTY;
+ }
+ }
+
+ private final class MapEntry implements Entry<String, Object> {
+ private final int index;
+
+ MapEntry(int index) {
+ this.index = index;
+ }
+
+ @Override
+ public String getKey() {
+ return keys[index];
+ }
+
+ @Override
+ public Object getValue() {
+ return values[index];
+ }
+
+ @Override
+ public Object setValue(Object value) {
+ Object old = values[index];
+ values[index] = value;
+ return old;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Entry<?, ?> e)) {
+ return false;
+ }
+ return keys[index].equals(e.getKey()) &&
Objects.equals(values[index], e.getValue());
+ }
+
+ @Override
+ public int hashCode() {
+ return keys[index].hashCode() ^ Objects.hashCode(values[index]);
+ }
+ }
+
+ @Serial
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ out.writeInt(size);
+ for (int i = 0; i < usedSlots; i++) {
+ if (keys[i] != null) {
+ out.writeObject(keys[i]);
+ out.writeObject(values[i]);
+ }
+ }
+ }
+
+ @Serial
+ private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
+ in.defaultReadObject();
+ int count = in.readInt();
+ init(tableSizeFor(Math.max((int) (count / LOAD_FACTOR) + 1,
DEFAULT_CAPACITY)));
+ for (int i = 0; i < count; i++) {
+ String key = (String) in.readObject();
+ Object value = in.readObject();
+ put(key, value);
+ }
+ }
+
}
diff --git
a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/constant-provider.vm
b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/constant-provider.vm
index 58cd2e162053..9041f833b93e 100644
---
a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/constant-provider.vm
+++
b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/constant-provider.vm
@@ -17,10 +17,12 @@
/* Generated by camel build tools - do NOT edit this file! */
package ${pn};
-import javax.annotation.processing.Generated;
+import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
+import javax.annotation.processing.Generated;
+
import org.jspecify.annotations.Nullable;
/**
@@ -41,5 +43,9 @@ public class ${cn} {
public static @Nullable String lookup(String key) {
return MAP.get(key);
}
+
+ public static Collection<String> values() {
+ return MAP.values();
+ }
}