001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.util; 018 019import java.io.IOException; 020import java.io.ObjectInputStream; 021import java.io.ObjectOutputStream; 022import java.io.Serial; 023import java.io.Serializable; 024import java.util.AbstractMap; 025import java.util.AbstractSet; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.Iterator; 029import java.util.Map; 030import java.util.NoSuchElementException; 031import java.util.Objects; 032import java.util.Set; 033 034/** 035 * A map that uses case insensitive keys, but preserves the original key cases. 036 * <p/> 037 * The map uses a custom hash table with case-insensitive hashing and comparison, providing O(1) for {@code get}, 038 * {@code put}, {@code containsKey} and {@code remove} operations without allocating temporary strings. Entries are 039 * stored in insertion order. 040 * <p/> 041 * This map is <b>not</b> designed to be thread safe as concurrent access to it is not supposed to be performed by the 042 * Camel routing engine. 043 */ 044public class CaseInsensitiveMap extends AbstractMap<String, Object> implements Serializable { 045 046 private static final @Serial long serialVersionUID = -8538318195477618308L; 047 private static final int DEFAULT_CAPACITY = 16; 048 private static final float LOAD_FACTOR = 0.75f; 049 private static final int EMPTY = -1; 050 051 // Static lookup table for deduplicating well-known header keys (e.g. Exchange constants). 052 // Registered once at startup; read-only after that. Zero-allocation lookups. 053 private static volatile int[] knownTable; 054 private static volatile String[] knownEntries; 055 private static volatile int[] knownChainNext; 056 private static volatile int knownMask; 057 058 /** 059 * Registers a set of well-known header key strings for deduplication. When a key passed to {@link #put} matches one 060 * of these strings (case-insensitive), the canonical reference from this set is stored instead of the caller's 061 * string, reducing memory when many map instances carry the same headers (e.g. deserialized exchanges). 062 * <p/> 063 * This method is intended to be called once during framework startup. 064 */ 065 public static void registerKnownKeys(Collection<String> keys) { 066 int sz = keys.size(); 067 int tableSize = tableSizeFor(Math.max((int) (sz / LOAD_FACTOR) + 1, DEFAULT_CAPACITY)); 068 int mask = tableSize - 1; 069 int[] tbl = new int[tableSize]; 070 Arrays.fill(tbl, EMPTY); 071 String[] entries = keys.toArray(new String[0]); 072 int[] chain = new int[entries.length]; 073 074 for (int i = 0; i < entries.length; i++) { 075 int b = caseInsensitiveHash(entries[i]) & mask; 076 chain[i] = tbl[b]; 077 tbl[b] = i; 078 } 079 080 knownEntries = entries; 081 knownChainNext = chain; 082 knownMask = mask; 083 // assign table last — readers check knownTable != null as the gate 084 knownTable = tbl; 085 } 086 087 private static String deduplicateKey(String key, int hash) { 088 int[] tbl = knownTable; 089 if (tbl == null) { 090 return key; 091 } 092 int idx = tbl[hash & knownMask]; 093 while (idx != EMPTY) { 094 if (knownEntries[idx].equalsIgnoreCase(key)) { 095 return knownEntries[idx]; 096 } 097 idx = knownChainNext[idx]; 098 } 099 return key; 100 } 101 102 private transient int[] table; 103 private transient String[] keys; 104 private transient Object[] values; 105 private transient int[] chainNext; 106 107 private transient int size; 108 private transient int usedSlots; 109 private transient int threshold; 110 111 public CaseInsensitiveMap() { 112 init(DEFAULT_CAPACITY); 113 } 114 115 public CaseInsensitiveMap(Map<? extends String, ?> map) { 116 init(tableSizeFor(Math.max((int) (map.size() / LOAD_FACTOR) + 1, DEFAULT_CAPACITY))); 117 putAll(map); 118 } 119 120 private void init(int tableCapacity) { 121 table = new int[tableCapacity]; 122 Arrays.fill(table, EMPTY); 123 int entryCapacity = (int) (tableCapacity * LOAD_FACTOR) + 1; 124 keys = new String[entryCapacity]; 125 values = new Object[entryCapacity]; 126 chainNext = new int[entryCapacity]; 127 size = 0; 128 usedSlots = 0; 129 threshold = (int) (tableCapacity * LOAD_FACTOR); 130 } 131 132 private static int tableSizeFor(int cap) { 133 int n = cap - 1; 134 n |= n >>> 1; 135 n |= n >>> 2; 136 n |= n >>> 4; 137 n |= n >>> 8; 138 n |= n >>> 16; 139 return Math.max(DEFAULT_CAPACITY, n + 1); 140 } 141 142 static int caseInsensitiveHash(String key) { 143 int h = 0; 144 for (int i = 0, len = key.length(); i < len; i++) { 145 char c = key.charAt(i); 146 if (c >= 'A' && c <= 'Z') { 147 c += 32; // fast ASCII upper-to-lower 148 } else if (c >= 128) { 149 // full Unicode two-step fold for non-ASCII 150 c = Character.toLowerCase(Character.toUpperCase(c)); 151 } 152 h = 31 * h + c; 153 } 154 return h ^ (h >>> 16); 155 } 156 157 private int findIndex(String key) { 158 int idx = table[caseInsensitiveHash(key) & (table.length - 1)]; 159 while (idx != EMPTY) { 160 if (keys[idx].equalsIgnoreCase(key)) { 161 return idx; 162 } 163 idx = chainNext[idx]; 164 } 165 return EMPTY; 166 } 167 168 private int findIndex(String key, int hash) { 169 int idx = table[hash & (table.length - 1)]; 170 while (idx != EMPTY) { 171 if (keys[idx].equalsIgnoreCase(key)) { 172 return idx; 173 } 174 idx = chainNext[idx]; 175 } 176 return EMPTY; 177 } 178 179 @Override 180 public Object get(Object key) { 181 int idx = findIndex((String) key); 182 return idx != EMPTY ? values[idx] : null; 183 } 184 185 @Override 186 public boolean containsKey(Object key) { 187 return findIndex((String) key) != EMPTY; 188 } 189 190 @Override 191 public boolean containsValue(Object value) { 192 for (int i = 0; i < usedSlots; i++) { 193 if (keys[i] != null && Objects.equals(value, values[i])) { 194 return true; 195 } 196 } 197 return false; 198 } 199 200 @Override 201 public Object put(String key, Object value) { 202 int hash = caseInsensitiveHash(key); 203 key = deduplicateKey(key, hash); 204 int idx = findIndex(key, hash); 205 if (idx != EMPTY) { 206 Object old = values[idx]; 207 values[idx] = value; 208 return old; 209 } 210 if (size >= threshold) { 211 resize(table.length * 2); 212 // table length changed, but hash is still valid 213 } 214 if (usedSlots >= keys.length) { 215 int newCap = keys.length + (keys.length >> 1); 216 keys = Arrays.copyOf(keys, newCap); 217 values = Arrays.copyOf(values, newCap); 218 chainNext = Arrays.copyOf(chainNext, newCap); 219 } 220 int slot = usedSlots++; 221 keys[slot] = key; 222 values[slot] = value; 223 int b = hash & (table.length - 1); 224 chainNext[slot] = table[b]; 225 table[b] = slot; 226 size++; 227 return null; 228 } 229 230 @Override 231 public Object remove(Object key) { 232 int hash = caseInsensitiveHash((String) key); 233 int b = hash & (table.length - 1); 234 int prev = EMPTY; 235 int cur = table[b]; 236 while (cur != EMPTY) { 237 if (keys[cur].equalsIgnoreCase((String) key)) { 238 Object old = values[cur]; 239 if (prev == EMPTY) { 240 table[b] = chainNext[cur]; 241 } else { 242 chainNext[prev] = chainNext[cur]; 243 } 244 keys[cur] = null; 245 values[cur] = null; 246 size--; 247 return old; 248 } 249 prev = cur; 250 cur = chainNext[cur]; 251 } 252 return null; 253 } 254 255 private void removeByIndex(int idx) { 256 String key = keys[idx]; 257 int b = caseInsensitiveHash(key) & (table.length - 1); 258 int prev = EMPTY; 259 int cur = table[b]; 260 while (cur != EMPTY) { 261 if (cur == idx) { 262 if (prev == EMPTY) { 263 table[b] = chainNext[idx]; 264 } else { 265 chainNext[prev] = chainNext[idx]; 266 } 267 break; 268 } 269 prev = cur; 270 cur = chainNext[cur]; 271 } 272 keys[idx] = null; 273 values[idx] = null; 274 size--; 275 } 276 277 private void resize(int newTableCapacity) { 278 int[] newTable = new int[newTableCapacity]; 279 Arrays.fill(newTable, EMPTY); 280 int entryCap = Math.max((int) (newTableCapacity * LOAD_FACTOR) + 1, size + 1); 281 String[] newKeys = new String[entryCap]; 282 Object[] newValues = new Object[entryCap]; 283 int[] newChainNext = new int[entryCap]; 284 285 int newSlot = 0; 286 for (int i = 0; i < usedSlots; i++) { 287 if (keys[i] != null) { 288 newKeys[newSlot] = keys[i]; 289 newValues[newSlot] = values[i]; 290 int b = caseInsensitiveHash(keys[i]) & (newTableCapacity - 1); 291 newChainNext[newSlot] = newTable[b]; 292 newTable[b] = newSlot; 293 newSlot++; 294 } 295 } 296 297 table = newTable; 298 keys = newKeys; 299 values = newValues; 300 chainNext = newChainNext; 301 usedSlots = newSlot; 302 threshold = (int) (newTableCapacity * LOAD_FACTOR); 303 } 304 305 @Override 306 public int size() { 307 return size; 308 } 309 310 @Override 311 public boolean isEmpty() { 312 return size == 0; 313 } 314 315 @Override 316 public void clear() { 317 Arrays.fill(table, EMPTY); 318 Arrays.fill(keys, 0, usedSlots, null); 319 Arrays.fill(values, 0, usedSlots, null); 320 size = 0; 321 usedSlots = 0; 322 } 323 324 @Override 325 public void putAll(Map<? extends String, ?> m) { 326 for (Entry<? extends String, ?> entry : m.entrySet()) { 327 put(entry.getKey(), entry.getValue()); 328 } 329 } 330 331 @Override 332 public Set<Entry<String, Object>> entrySet() { 333 return new EntrySet(); 334 } 335 336 private final class EntrySet extends AbstractSet<Entry<String, Object>> { 337 @Override 338 public int size() { 339 return size; 340 } 341 342 @Override 343 public boolean contains(Object o) { 344 if (!(o instanceof Entry<?, ?> e)) { 345 return false; 346 } 347 int idx = findIndex((String) e.getKey()); 348 return idx != EMPTY && Objects.equals(values[idx], e.getValue()); 349 } 350 351 @Override 352 public boolean remove(Object o) { 353 if (!(o instanceof Entry<?, ?> e)) { 354 return false; 355 } 356 int idx = findIndex((String) e.getKey()); 357 if (idx != EMPTY && Objects.equals(values[idx], e.getValue())) { 358 removeByIndex(idx); 359 return true; 360 } 361 return false; 362 } 363 364 @Override 365 public void clear() { 366 CaseInsensitiveMap.this.clear(); 367 } 368 369 @Override 370 public Iterator<Entry<String, Object>> iterator() { 371 return new EntryIterator(); 372 } 373 } 374 375 private final class EntryIterator implements Iterator<Entry<String, Object>> { 376 private int cursor; 377 private int lastReturned = EMPTY; 378 379 EntryIterator() { 380 cursor = advance(0); 381 } 382 383 private int advance(int from) { 384 for (int i = from; i < usedSlots; i++) { 385 if (keys[i] != null) { 386 return i; 387 } 388 } 389 return EMPTY; 390 } 391 392 @Override 393 public boolean hasNext() { 394 return cursor != EMPTY; 395 } 396 397 @Override 398 public Entry<String, Object> next() { 399 if (cursor == EMPTY) { 400 throw new NoSuchElementException(); 401 } 402 lastReturned = cursor; 403 cursor = advance(cursor + 1); 404 return new MapEntry(lastReturned); 405 } 406 407 @Override 408 public void remove() { 409 if (lastReturned == EMPTY) { 410 throw new IllegalStateException(); 411 } 412 removeByIndex(lastReturned); 413 lastReturned = EMPTY; 414 } 415 } 416 417 private final class MapEntry implements Entry<String, Object> { 418 private final int index; 419 420 MapEntry(int index) { 421 this.index = index; 422 } 423 424 @Override 425 public String getKey() { 426 return keys[index]; 427 } 428 429 @Override 430 public Object getValue() { 431 return values[index]; 432 } 433 434 @Override 435 public Object setValue(Object value) { 436 Object old = values[index]; 437 values[index] = value; 438 return old; 439 } 440 441 @Override 442 public boolean equals(Object o) { 443 if (!(o instanceof Entry<?, ?> e)) { 444 return false; 445 } 446 return keys[index].equals(e.getKey()) && Objects.equals(values[index], e.getValue()); 447 } 448 449 @Override 450 public int hashCode() { 451 return keys[index].hashCode() ^ Objects.hashCode(values[index]); 452 } 453 } 454 455 @Serial 456 private void writeObject(ObjectOutputStream out) throws IOException { 457 out.defaultWriteObject(); 458 out.writeInt(size); 459 for (int i = 0; i < usedSlots; i++) { 460 if (keys[i] != null) { 461 out.writeObject(keys[i]); 462 out.writeObject(values[i]); 463 } 464 } 465 } 466 467 @Serial 468 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 469 in.defaultReadObject(); 470 int count = in.readInt(); 471 init(tableSizeFor(Math.max((int) (count / LOAD_FACTOR) + 1, DEFAULT_CAPACITY))); 472 for (int i = 0; i < count; i++) { 473 String key = (String) in.readObject(); 474 Object value = in.readObject(); 475 put(key, value); 476 } 477 } 478 479}