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}