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 }