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 }