001package com.randomnoun.common;
002
003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
005 */
006
007import java.util.HashMap;
008import java.util.Map;
009
010/** Case-preserving hashmap that allows case-insensitive lookups.
011 * 
012 * @author knoxg
013 * 
014 * 
015 */
016public class HashMapIgnoreCase<K, V> extends HashMap<String, V> {
017    
018        /** generated  serialVersionUID */
019        private static final long serialVersionUID = -6469387933015736361L;
020        
021        /** an extra map of lower-case keys to case-sensitive keys */
022        private Map<String, String> lowercase = new HashMap<String, String>();
023        
024        /**
025         * Retrieve a value from the cache.
026         *
027         * @param key key whose associated value is to be returned
028         *
029         * @return the value to which this map maps the specified key, or
030         *   null if the map contains no mapping for this key.
031         */
032        @Override
033        public V get(Object key)
034        {
035                // System.out.println("getting " + lowercase.get(key.toLowerCase()));
036                return super.get(lowercase.get(((String)key).toLowerCase()));
037        }
038        
039        /**
040         * Associates the specified value with the specified key in this map.
041         * If the map previously contained a mapping for this key, the old
042         * value is replaced.
043         *
044         * @param key key with which the specified value is to be associated.
045         * @param value value to be associated with the specified key.
046         * 
047         * @return previous value associated with specified key, or <code>null</code>
048         *             if there was no mapping for key.  A <code>null</code> return can
049         *             also indicate that the HashMap previously associated
050         *             <code>null</code> with the specified key.
051         *
052         * @throws IllegalArgumentException if the key parameter is not a String
053         */
054        @Override
055        public V put(String key, V value) {
056                if (!(key instanceof String)) {
057                        throw new IllegalArgumentException("HashMapIgnoreCase keys must be Strings");
058                }
059                
060                String lowercaseKey = ((String)key).toLowerCase();
061                V previousEntry = null;
062                
063                // remove any existing entry with the same case-insensitive value
064                if (lowercase.containsKey(lowercaseKey)) {
065                        //System.out.println("Removing " + lowercase.get(lowercaseKey));
066                        previousEntry = super.remove(lowercase.get(lowercaseKey));
067                }
068                lowercase.put(lowercaseKey, key);
069                //System.out.println("putting " + key + "/" + value);
070                super.put(key, value);
071                return previousEntry;
072        }
073        
074        /** Returns true if this map contains a mapping for the specified key. 
075         * More formally, returns true if and only if this map contains at 
076         * a mapping for a key <code>k</code> such that 
077         * (<code>key==null ? k==null : key.equals(k)</code>). (There can be at 
078         * most one such mapping.)
079         *  
080         * @param key key whose presence in this map is to be tested. 
081         * 
082         * @return true if this map contains a mapping for the specified key. 
083         */
084        @Override
085        public boolean containsKey(Object key) {
086                if (!(key instanceof String)) {
087                        return false;
088                }
089                String lowercaseKey = ((String)key).toLowerCase();
090                return lowercase.containsKey(lowercaseKey);
091        }
092        
093
094        /**
095         * Removes the mapping for this key from this map if present.
096         *
097         * @param  key key whose mapping is to be removed from the map.
098         * @return previous value associated with specified key, or <code>null</code>
099         *             if there was no mapping for key.  A <code>null</code> return can
100         *             also indicate that the map previously associated <code>null</code>
101         *             with the specified key.
102         */
103        @Override
104        public V remove(Object key) {
105                V previousEntry;
106                String lowercaseKey = ((String) key).toLowerCase();
107                previousEntry = super.remove(lowercase.get(lowercaseKey));
108                lowercase.remove(lowercaseKey);
109                return previousEntry;
110        }
111        
112
113}