001    package com.mockrunner.util.common;
002    
003    import java.util.Collection;
004    import java.util.HashMap;
005    import java.util.Iterator;
006    import java.util.Map;
007    import java.util.Set;
008    
009    /**
010     * Implementation of a <code>Map</code> that recognizes the case of the
011     * keys, if the keys are strings. If <code>isCaseSensitive</code> is
012     * <code>true</code> it behaves exactly like a <code>HashMap</code>.
013     * If <code>isCaseSensitive</code> is <code>false</code> (which is the
014     * default), it considers same strings with different case as equal.
015     * I.e. if you do
016     * <br>
017     * <br>
018     * <code>put("test", "1");</code>
019     * <br>
020     * <code>put("TEST", "2");</code>
021     * <br>
022     * <br>
023     * the second <code>put</code> overwrites the value of the first one, 
024     * because the keys are considered to be equal. With
025     * <br>
026     * <br>
027     * <code>get("TesT");</code>
028     * <br>
029     * <br>
030     * you'll get the result <code>"2"</code>.
031     * If you iterate through the keys (using either <code>keySet</code> or
032     * <code>entrySet</code>), you'll get the first added version of the key,
033     * in the above case, you'll get <code>"test"</code>.
034     * It is allowed to use non-strings as keys. In this case the <code>Map</code>
035     * behaves like a usual <code>HashMap</code>.<br>
036     * Note: This class is similar to a <code>TreeMap(String.CASE_INSENSITIVE_ORDER)</code>
037     *       except that non-strings do not throw a <code>ClassCastException</code>
038     *       and that keys are not sorted.
039     */
040    public class CaseAwareMap implements Map
041    {
042        private boolean isCaseSensitive;
043        private Map caseInsensitiveMap;
044        private Map actualMap;
045         
046        public CaseAwareMap()
047        {
048            this(false);
049        }
050        
051        public CaseAwareMap(boolean isCaseSensitive)
052        {
053            this.isCaseSensitive = isCaseSensitive;
054            caseInsensitiveMap = new HashMap();
055            actualMap = new HashMap();
056        }
057        
058        /**
059         * Returns if keys are case sensitive. Defaults to <code>false</code>.
060         * @return are keys case sensitive
061         */ 
062        public boolean isCaseSensitive()
063        {
064            return isCaseSensitive;
065        }
066        
067        /**
068         * Sets if keys are case sensitive.
069         * If set to <code>true</code> this implementation behaves like
070         * a <code>HashMap</code>. Please note, that all entries are cleared
071         * when switching case sensitivity. It's not possible to switch
072         * and keep the entries.
073         * @param isCaseSensitive are keys case sensitive
074         */ 
075        public void setCaseSensitive(boolean isCaseSensitive)
076        {
077            clear();
078            this.isCaseSensitive = isCaseSensitive;
079        }
080        
081        public void clear()
082        {
083            caseInsensitiveMap.clear();
084            actualMap.clear();
085        }
086        
087        public boolean containsKey(Object key)
088        {
089            Object compareKey = getCompareKey(key);
090            return getCompareMap().containsKey(compareKey);
091        }
092        
093        public boolean containsValue(Object value)
094        {
095            return actualMap.containsValue(value);
096        }
097        
098        public Set entrySet()
099        {
100            return actualMap.entrySet();
101        }
102        
103        public Object get(Object key)
104        {
105            Object compareKey = getCompareKey(key);
106            return getCompareMap().get(compareKey);
107        }
108        
109        public boolean isEmpty()
110        {
111            return size() <= 0;
112        }
113        
114        public Set keySet()
115        {
116            return actualMap.keySet();
117        }
118        
119        public Object put(Object key, Object value)
120        {
121            return doConsistentModify(key, new ConsistentPut(value));
122        }
123        
124        public void putAll(Map map)
125        {
126            Iterator keys = map.keySet().iterator();
127            while(keys.hasNext())
128            {
129                Object nextKey = keys.next();
130                Object nextValue = map.get(nextKey);
131                put(nextKey, nextValue);
132            }
133        }
134        
135        public Object remove(Object key)
136        {
137            return doConsistentModify(key, new ConsistentRemove());
138        }
139        
140        public int size()
141        {
142            return actualMap.size();
143        }
144        
145        public Collection values()
146        {
147            return actualMap.values();
148        }
149        
150        private boolean areKeysEquals(Object actualKey, Object compareKey)
151        {
152            if(null == actualKey && null == compareKey) return true;
153            if(null == actualKey) return false;
154            if(null == compareKey) return false;
155            Object actualCompareKey = getCompareKey(actualKey);
156            return compareKey.equals(actualCompareKey);
157        }
158        
159        private boolean isStringKey(Object key)
160        {
161            return (null != key) && (key instanceof String);
162        }
163        
164        private Object getCompareKey(Object key)
165        {
166            if(isCaseSensitive || !isStringKey(key))
167            {
168                return key;
169            }
170            return ((String)key).toUpperCase();
171        }
172        
173        private Map getCompareMap()
174        {
175            if(isCaseSensitive)
176            {
177                return actualMap;
178            }
179            return caseInsensitiveMap;
180        }
181        
182        private Object doConsistentModify(Object key, ConsistentModify modifier)
183        {
184            Object compareKey = getCompareKey(key);
185            if(!caseInsensitiveMap.containsKey(compareKey))
186            {
187                return modifier.modify(key, compareKey);
188            }
189            Iterator iterator = actualMap.keySet().iterator();
190            while(iterator.hasNext())
191            {
192                Object actualKey = iterator.next();
193                if(areKeysEquals(actualKey, compareKey))
194                {
195                    return modifier.modify(actualKey, compareKey);
196                }
197            }
198            return null;
199        }
200        
201        private interface ConsistentModify
202        {
203            public Object modify(Object key1, Object key2);
204        }
205        
206        private class ConsistentRemove implements ConsistentModify
207        {
208            public Object modify(Object key1, Object key2)
209            {
210                actualMap.remove(key1);
211                return caseInsensitiveMap.remove(key2);
212            }
213        }
214        
215        private class ConsistentPut implements ConsistentModify
216        {
217            private Object value;
218            
219            public ConsistentPut(Object value)
220            {
221                this.value = value;
222            }
223            
224            public Object modify(Object key1, Object key2)
225            {
226                actualMap.put(key1, value);
227                return caseInsensitiveMap.put(key2, value);
228            }
229        }
230    }