001// Copyright 2007 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.ioc.util;
016
017import java.io.Serializable;
018import java.util.*;
019
020/**
021 * An mapped collection where the keys are always strings and access to values is case-insensitive. The case of keys in
022 * the map is <em>maintained</em>, but on any access to a key (directly or indirectly), all key comparisons are
023 * performed in a case-insensitive manner. The map implementation is intended to support a reasonably finite number
024 * (dozens or hundreds, not thousands or millions of key/value pairs. Unlike HashMap, it is based on a sorted list of
025 * entries rather than hash bucket. It is also geared towards a largely static map, one that is created and then used
026 * without modification.
027 *
028 * @param <V> the type of value stored
029 */
030public class CaseInsensitiveMap<V> extends AbstractMap<String, V> implements Serializable
031{
032
033    private static final long serialVersionUID = -3162531976817110908L;
034
035    private static final int NULL_HASH = Integer.MIN_VALUE;
036
037    private static final int DEFAULT_SIZE = 20;
038
039    private static class CIMEntry<V> implements Map.Entry<String, V>, Serializable
040    {
041        private static final long serialVersionUID = 6713986085221148350L;
042
043        private String key;
044
045        private final int hashCode;
046
047        V value;
048
049        public CIMEntry(final String key, final int hashCode, V value)
050        {
051            this.key = key;
052            this.hashCode = hashCode;
053            this.value = value;
054        }
055
056        @Override
057        public String getKey()
058        {
059            return key;
060        }
061
062        @Override
063        public V getValue()
064        {
065            return value;
066        }
067
068        @Override
069        public V setValue(V value)
070        {
071            V result = this.value;
072
073            this.value = value;
074
075            return result;
076        }
077
078        /**
079         * Returns true if both keys are null, or if the provided key is the same as, or case-insensitively equal to,
080         * the entrie's key.
081         *
082         * @param key to compare against
083         * @return true if equal
084         */
085        @SuppressWarnings({ "StringEquality" })
086        boolean matches(String key)
087        {
088            return key == this.key || (key != null && key.equalsIgnoreCase(this.key));
089        }
090
091        boolean valueMatches(Object value)
092        {
093            return value == this.value || (value != null && value.equals(this.value));
094        }
095
096        @Override
097        public String toString() {
098          StringBuilder builder = new StringBuilder();
099          builder.append("CIMEntry [key=").append(key).append(", value=").append(value).append(']');
100          return builder.toString();
101        }
102
103    }
104
105    private class EntrySetIterator implements Iterator<Entry<String, V>>
106    {
107        int expectedModCount = modCount;
108
109        int index;
110
111        int current = -1;
112
113        @Override
114        public boolean hasNext()
115        {
116            return index < size;
117        }
118
119        @Override
120        public Entry<String, V> next()
121        {
122            check();
123
124            if (index >= size) throw new NoSuchElementException();
125
126            current = index++;
127
128            return entries[current];
129        }
130
131        @Override
132        public void remove()
133        {
134            check();
135
136            if (current < 0) throw new NoSuchElementException();
137
138            new Position(current, true).remove();
139
140            expectedModCount = modCount;
141        }
142
143        private void check()
144        {
145            if (expectedModCount != modCount) throw new ConcurrentModificationException();
146        }
147
148        @Override
149        public String toString() {
150           return "EntrySetIterator, current key: "+entries[current].key;
151        }
152    }
153
154    private class EntrySet extends AbstractSet<Entry<String, V>>
155    {
156        @Override
157        public Iterator<Entry<String, V>> iterator()
158        {
159            return new EntrySetIterator();
160        }
161
162        @Override
163        public int size()
164        {
165            return size;
166        }
167
168        @Override
169        public void clear()
170        {
171            CaseInsensitiveMap.this.clear();
172        }
173
174        @Override
175        public boolean contains(Object o)
176        {
177            if (!(o instanceof Map.Entry)) return false;
178
179            @SuppressWarnings("rawtypes")
180            Map.Entry e = (Map.Entry) o;
181
182            Position position = select(e.getKey());
183
184            return position.isFound() && position.entry().valueMatches(e.getValue());
185        }
186
187        @Override
188        public boolean remove(Object o)
189        {
190            if (!(o instanceof Map.Entry)) return false;
191
192            @SuppressWarnings("rawtypes")
193            Map.Entry e = (Map.Entry) o;
194
195            Position position = select(e.getKey());
196
197            if (position.isFound() && position.entry().valueMatches(e.getValue()))
198            {
199                position.remove();
200                return true;
201            }
202
203            return false;
204        }
205
206    }
207
208    private class Position
209    {
210        private final int cursor;
211
212        private final boolean found;
213
214        Position(int cursor, boolean found)
215        {
216            this.cursor = cursor;
217            this.found = found;
218        }
219
220        boolean isFound()
221        {
222            return found;
223        }
224
225        CIMEntry<V> entry()
226        {
227            return entries[cursor];
228        }
229
230        V get()
231        {
232            return found ? entries[cursor].value : null;
233        }
234
235        V remove()
236        {
237            if (!found) return null;
238
239            V result = entries[cursor].value;
240
241            // Remove the entry by shifting everything else down.
242
243            System.arraycopy(entries, cursor + 1, entries, cursor, size - cursor - 1);
244
245            // We shifted down, leaving one (now duplicate) entry behind.
246
247            entries[--size] = null;
248
249            // A structural change for sure
250
251            modCount++;
252
253            return result;
254        }
255
256        @SuppressWarnings("unchecked")
257        V put(String key, int hashCode, V newValue)
258        {
259            if (found)
260            {
261                CIMEntry<V> e = entries[cursor];
262
263                V result = e.value;
264
265                // Not a structural change, so no change to modCount
266
267                // Update the key (to maintain case). By definition, the hash code
268                // will not change.
269
270                e.key = key;
271                e.value = newValue;
272
273                return result;
274            }
275
276            // Not found, we're going to add it.
277
278            int newSize = size + 1;
279
280            if (newSize == entries.length)
281            {
282                // Time to expand!
283
284                int newCapacity = (size * 3) / 2 + 1;
285
286                CIMEntry<V>[] newEntries = new CIMEntry[newCapacity];
287
288                System.arraycopy(entries, 0, newEntries, 0, cursor);
289
290                System.arraycopy(entries, cursor, newEntries, cursor + 1, size - cursor);
291
292                entries = newEntries;
293            }
294            else
295            {
296                // Open up a space for the new entry
297
298                System.arraycopy(entries, cursor, entries, cursor + 1, size - cursor);
299            }
300
301            CIMEntry<V> newEntry = new CIMEntry<V>(key, hashCode, newValue);
302            entries[cursor] = newEntry;
303
304            size++;
305
306            // This is definately a structural change
307
308            modCount++;
309
310            return null;
311        }
312
313    }
314
315    // The list of entries. This is kept sorted by hash code. In some cases, there may be different
316    // keys with the same hash code in adjacent indexes.
317    private CIMEntry<V>[] entries;
318
319    private int size = 0;
320
321    // Used by iterators to check for concurrent modifications
322
323    private transient int modCount = 0;
324
325    private transient Set<Map.Entry<String, V>> entrySet;
326
327    public CaseInsensitiveMap()
328    {
329        this(DEFAULT_SIZE);
330    }
331
332    @SuppressWarnings("unchecked")
333    public CaseInsensitiveMap(int size)
334    {
335        entries = new CIMEntry[Math.max(size, 3)];
336    }
337
338    public CaseInsensitiveMap(Map<String, ? extends V> map)
339    {
340        this(map.size());
341
342        for (Map.Entry<String, ? extends V> entry : map.entrySet())
343        {
344            put(entry.getKey(), entry.getValue());
345        }
346    }
347
348    @Override
349    public void clear()
350    {
351        for (int i = 0; i < size; i++)
352            entries[i] = null;
353
354        size = 0;
355        modCount++;
356    }
357
358    @Override
359    public boolean isEmpty()
360    {
361        return size == 0;
362    }
363
364    @Override
365    public int size()
366    {
367        return size;
368    }
369
370    @Override
371    public V put(String key, V value)
372    {
373        int hashCode = caseInsenitiveHashCode(key);
374
375        return select(key, hashCode).put(key, hashCode, value);
376    }
377
378    @Override
379    public boolean containsKey(Object key)
380    {
381        return select(key).isFound();
382    }
383
384    @Override
385    public V get(Object key)
386    {
387        return select(key).get();
388    }
389
390    @Override
391    public V remove(Object key)
392    {
393        return select(key).remove();
394    }
395
396    @Override
397    public Set<Map.Entry<String, V>> entrySet()
398    {
399        if (entrySet == null) entrySet = new EntrySet();
400
401        return entrySet;
402    }
403
404    @Override
405    public Set<String> keySet() {
406
407        AbstractSet<String> set = new AbstractSet<String>() {
408
409          @Override
410          public Iterator<String> iterator() {
411
412            final Iterator<java.util.Map.Entry<String, V>> entrySetIterator = entrySet().iterator();
413
414            return new Iterator<String>() {
415              @Override
416              public boolean hasNext() {
417                return entrySetIterator.hasNext();
418              }
419              @Override
420              public String next() {
421                String nextKey = entrySetIterator.next().getKey();
422                return nextKey;
423              }
424
425              @Override
426              public void remove() {
427                throw new UnsupportedOperationException("Modifications to the key set are not allowed.");
428              }
429            };
430          }
431
432          @Override
433          public boolean contains(Object o) {
434            return containsKey(o);
435          }
436
437          @Override
438          public int size() {
439            return size;
440          }
441        };
442
443        return Collections.unmodifiableSet(set);
444    }
445
446    private Position select(Object key)
447    {
448        if (key == null || key instanceof String)
449        {
450            String keyString = (String) key;
451            return select(keyString, caseInsenitiveHashCode(keyString));
452        }
453
454        return new Position(0, false);
455    }
456
457    /**
458     * Searches the elements for the index of the indicated key and (case insensitive) hash code. Sets the _cursor and
459     * _found attributes.
460     */
461    private Position select(String key, int hashCode)
462    {
463        if (size == 0) return new Position(0, false);
464
465        int low = 0;
466        int high = size - 1;
467
468        int cursor;
469
470        while (low <= high)
471        {
472            cursor = (low + high) >> 1;
473
474            CIMEntry<V> e = entries[cursor];
475
476            if (e.hashCode < hashCode)
477            {
478                low = cursor + 1;
479                continue;
480            }
481
482            if (e.hashCode > hashCode)
483            {
484                high = cursor - 1;
485                continue;
486            }
487
488            return tunePosition(key, hashCode, cursor);
489        }
490
491        return new Position(low, false);
492    }
493
494    /**
495     * select() has located a matching hashCode, but there's an outlying possibility that multiple keys share the same
496     * hashCode. Backup the cursor until we get to locate the initial hashCode match, then march forward until the key
497     * is located, or the hashCode stops matching.
498     *
499     * @param key
500     * @param hashCode
501     */
502    private Position tunePosition(String key, int hashCode, int cursor)
503    {
504        boolean found = false;
505
506        while (cursor > 0)
507        {
508            if (entries[cursor - 1].hashCode != hashCode) break;
509
510            cursor--;
511        }
512
513        while (true)
514        {
515            if (entries[cursor].matches(key))
516            {
517                found = true;
518                break;
519            }
520
521            // Advance to the next entry.
522
523            cursor++;
524
525            // If out of entries,
526            if (cursor >= size || entries[cursor].hashCode != hashCode) break;
527        }
528
529        return new Position(cursor, found);
530    }
531
532    static int caseInsenitiveHashCode(String input)
533    {
534        if (input == null) return NULL_HASH;
535
536        int length = input.length();
537        int hash = 0;
538
539        // This should end up more or less equal to input.toLowerCase().hashCode(), unless String
540        // changes its implementation. Let's hope this is reasonably fast.
541
542        for (int i = 0; i < length; i++)
543        {
544            int ch = input.charAt(i);
545
546            int caselessCh = Character.toLowerCase(ch);
547
548            hash = 31 * hash + caselessCh;
549        }
550
551        return hash;
552    }
553
554}