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>.
036 */
037 public class CaseAwareMap implements Map
038 {
039 private boolean isCaseSensitive;
040 private Map caseInsensitiveMap;
041 private Map actualMap;
042
043 public CaseAwareMap()
044 {
045 this(false);
046 }
047
048 public CaseAwareMap(boolean isCaseSensitive)
049 {
050 this.isCaseSensitive = isCaseSensitive;
051 caseInsensitiveMap = new HashMap();
052 actualMap = new HashMap();
053 }
054
055 /**
056 * Returns if keys are case sensitive. Defaults to <code>false</code>.
057 * @return are keys case sensitive
058 */
059 public boolean isCaseSensitive()
060 {
061 return isCaseSensitive;
062 }
063
064 /**
065 * Sets if keys are case sensitive.
066 * If set to <code>true</code> this implementation behaves like
067 * a <code>HashMap</code>. Please note, that all entries are cleared
068 * when switching case sensitivity. It's not possible to switch
069 * and keep the entries.
070 * @param isCaseSensitive are keys case sensitive
071 */
072 public void setCaseSensitive(boolean isCaseSensitive)
073 {
074 clear();
075 this.isCaseSensitive = isCaseSensitive;
076 }
077
078 public void clear()
079 {
080 caseInsensitiveMap.clear();
081 actualMap.clear();
082 }
083
084 public boolean containsKey(Object key)
085 {
086 Object compareKey = getCompareKey(key);
087 return getCompareMap().containsKey(compareKey);
088 }
089
090 public boolean containsValue(Object value)
091 {
092 return actualMap.containsValue(value);
093 }
094
095 public Set entrySet()
096 {
097 return actualMap.entrySet();
098 }
099
100 public Object get(Object key)
101 {
102 Object compareKey = getCompareKey(key);
103 return getCompareMap().get(compareKey);
104 }
105
106 public boolean isEmpty()
107 {
108 return size() <= 0;
109 }
110
111 public Set keySet()
112 {
113 return actualMap.keySet();
114 }
115
116 public Object put(Object key, Object value)
117 {
118 return doConsistentModify(key, new ConsistentPut(value));
119 }
120
121 public void putAll(Map map)
122 {
123 Iterator keys = map.keySet().iterator();
124 while(keys.hasNext())
125 {
126 Object nextKey = keys.next();
127 Object nextValue = map.get(nextKey);
128 put(nextKey, nextValue);
129 }
130 }
131
132 public Object remove(Object key)
133 {
134 return doConsistentModify(key, new ConsistentRemove());
135 }
136
137 public int size()
138 {
139 return actualMap.size();
140 }
141
142 public Collection values()
143 {
144 return actualMap.values();
145 }
146
147 private boolean areKeysEquals(Object actualKey, Object compareKey)
148 {
149 if(null == actualKey && null == compareKey) return true;
150 if(null == actualKey) return false;
151 if(null == compareKey) return false;
152 Object actualCompareKey = getCompareKey(actualKey);
153 return compareKey.equals(actualCompareKey);
154 }
155
156 private boolean isStringKey(Object key)
157 {
158 return (null != key) && (key instanceof String);
159 }
160
161 private Object getCompareKey(Object key)
162 {
163 if(isCaseSensitive || !isStringKey(key))
164 {
165 return key;
166 }
167 return ((String)key).toUpperCase();
168 }
169
170 private Map getCompareMap()
171 {
172 if(isCaseSensitive)
173 {
174 return actualMap;
175 }
176 return caseInsensitiveMap;
177 }
178
179 private Object doConsistentModify(Object key, ConsistentModify modifier)
180 {
181 Object compareKey = getCompareKey(key);
182 if(!caseInsensitiveMap.containsKey(compareKey))
183 {
184 return modifier.modify(key, compareKey);
185 }
186 Iterator iterator = actualMap.keySet().iterator();
187 while(iterator.hasNext())
188 {
189 Object actualKey = iterator.next();
190 if(areKeysEquals(actualKey, compareKey))
191 {
192 return modifier.modify(actualKey, compareKey);
193 }
194 }
195 return null;
196 }
197
198 private interface ConsistentModify
199 {
200 public Object modify(Object key1, Object key2);
201 }
202
203 private class ConsistentRemove implements ConsistentModify
204 {
205 public Object modify(Object key1, Object key2)
206 {
207 actualMap.remove(key1);
208 return caseInsensitiveMap.remove(key2);
209 }
210 }
211
212 private class ConsistentPut implements ConsistentModify
213 {
214 private Object value;
215
216 public ConsistentPut(Object value)
217 {
218 this.value = value;
219 }
220
221 public Object modify(Object key1, Object key2)
222 {
223 actualMap.put(key1, value);
224 return caseInsensitiveMap.put(key2, value);
225 }
226 }
227 }