001 // Copyright 2007 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry5.ioc.util; 016 017 import java.io.Serializable; 018 import java.util.*; 019 020 /** 021 * An mapped collection where the keys are always strings and access to values is case-insensitive. The case of keys in 022 * the map is <em>maintained</em>, but on any access to a key (directly or indirectly), all key comparisons are 023 * performed in a case-insensitive manner. The map implementation is intended to support a reasonably finite number 024 * (dozens or hundreds, not thousands or millions of key/value pairs. Unlike HashMap, it is based on a sorted list of 025 * entries rather than hash bucket. It is also geared towards a largely static map, one that is created and then used 026 * without modification. 027 * 028 * @param <V> the type of value stored 029 */ 030 public class CaseInsensitiveMap<V> extends AbstractMap<String, V> implements Serializable 031 { 032 private static final long serialVersionUID = 3362718337611953298L; 033 034 private static final int NULL_HASH = Integer.MIN_VALUE; 035 036 private static final int DEFAULT_SIZE = 20; 037 038 private static class CIMEntry<V> implements Map.Entry<String, V>, Serializable 039 { 040 private static final long serialVersionUID = 6713986085221148350L; 041 042 private String key; 043 044 private final int hashCode; 045 046 V value; 047 048 public CIMEntry(final String key, final int hashCode, V value) 049 { 050 this.key = key; 051 this.hashCode = hashCode; 052 this.value = value; 053 } 054 055 public String getKey() 056 { 057 return key; 058 } 059 060 public V getValue() 061 { 062 return value; 063 } 064 065 public V setValue(V value) 066 { 067 V result = this.value; 068 069 this.value = value; 070 071 return result; 072 } 073 074 /** 075 * Returns true if both keys are null, or if the provided key is the same as, or case-insensitively equal to, 076 * the entrie's key. 077 * 078 * @param key to compare against 079 * @return true if equal 080 */ 081 @SuppressWarnings({ "StringEquality" }) 082 boolean matches(String key) 083 { 084 return key == this.key || (key != null && key.equalsIgnoreCase(this.key)); 085 } 086 087 boolean valueMatches(Object value) 088 { 089 return value == this.value || (value != null && value.equals(this.value)); 090 } 091 } 092 093 private class EntrySetIterator implements Iterator 094 { 095 int expectedModCount = modCount; 096 097 int index; 098 099 int current = -1; 100 101 public boolean hasNext() 102 { 103 return index < size; 104 } 105 106 public Object next() 107 { 108 check(); 109 110 if (index >= size) throw new NoSuchElementException(); 111 112 current = index++; 113 114 return entries[current]; 115 } 116 117 public void remove() 118 { 119 check(); 120 121 if (current < 0) throw new NoSuchElementException(); 122 123 new Position(current, true).remove(); 124 125 expectedModCount = modCount; 126 } 127 128 private void check() 129 { 130 if (expectedModCount != modCount) throw new ConcurrentModificationException(); 131 } 132 } 133 134 @SuppressWarnings("unchecked") 135 private class EntrySet extends AbstractSet 136 { 137 @Override 138 public Iterator iterator() 139 { 140 return new EntrySetIterator(); 141 } 142 143 @Override 144 public int size() 145 { 146 return size; 147 } 148 149 @Override 150 public void clear() 151 { 152 CaseInsensitiveMap.this.clear(); 153 } 154 155 @Override 156 public boolean contains(Object o) 157 { 158 if (!(o instanceof Map.Entry)) return false; 159 160 Map.Entry e = (Map.Entry) o; 161 162 Position position = select(e.getKey()); 163 164 return position.isFound() && position.entry().valueMatches(e.getValue()); 165 } 166 167 @Override 168 public boolean remove(Object o) 169 { 170 if (!(o instanceof Map.Entry)) return false; 171 172 Map.Entry e = (Map.Entry) o; 173 174 Position position = select(e.getKey()); 175 176 if (position.isFound() && position.entry().valueMatches(e.getValue())) 177 { 178 position.remove(); 179 return true; 180 } 181 182 return false; 183 } 184 185 } 186 187 private class Position 188 { 189 private final int cursor; 190 191 private final boolean found; 192 193 Position(int cursor, boolean found) 194 { 195 this.cursor = cursor; 196 this.found = found; 197 } 198 199 boolean isFound() 200 { 201 return found; 202 } 203 204 CIMEntry<V> entry() 205 { 206 return entries[cursor]; 207 } 208 209 V get() 210 { 211 return found ? entries[cursor].value : null; 212 } 213 214 V remove() 215 { 216 if (!found) return null; 217 218 V result = entries[cursor].value; 219 220 // Remove the entry by shifting everything else down. 221 222 System.arraycopy(entries, cursor + 1, entries, cursor, size - cursor - 1); 223 224 // We shifted down, leaving one (now duplicate) entry behind. 225 226 entries[--size] = null; 227 228 // A structural change for sure 229 230 modCount++; 231 232 return result; 233 } 234 235 @SuppressWarnings("unchecked") 236 V put(String key, int hashCode, V newValue) 237 { 238 if (found) 239 { 240 CIMEntry<V> e = entries[cursor]; 241 242 V result = e.value; 243 244 // Not a structural change, so no change to modCount 245 246 // Update the key (to maintain case). By definition, the hash code 247 // will not change. 248 249 e.key = key; 250 e.value = newValue; 251 252 return result; 253 } 254 255 // Not found, we're going to add it. 256 257 int newSize = size + 1; 258 259 if (newSize == entries.length) 260 { 261 // Time to expand! 262 263 int newCapacity = (size * 3) / 2 + 1; 264 265 CIMEntry<V>[] newEntries = new CIMEntry[newCapacity]; 266 267 System.arraycopy(entries, 0, newEntries, 0, cursor); 268 269 System.arraycopy(entries, cursor, newEntries, cursor + 1, size - cursor); 270 271 entries = newEntries; 272 } 273 else 274 { 275 // Open up a space for the new entry 276 277 System.arraycopy(entries, cursor, entries, cursor + 1, size - cursor); 278 } 279 280 CIMEntry<V> newEntry = new CIMEntry<V>(key, hashCode, newValue); 281 entries[cursor] = newEntry; 282 283 size++; 284 285 // This is definately a structural change 286 287 modCount++; 288 289 return null; 290 } 291 292 } 293 294 // The list of entries. This is kept sorted by hash code. In some cases, there may be different 295 // keys with the same hash code in adjacent indexes. 296 private CIMEntry<V>[] entries; 297 298 private int size = 0; 299 300 // Used by iterators to check for concurrent modifications 301 302 private transient int modCount = 0; 303 304 private transient Set<Map.Entry<String, V>> entrySet; 305 306 public CaseInsensitiveMap() 307 { 308 this(DEFAULT_SIZE); 309 } 310 311 @SuppressWarnings("unchecked") 312 public CaseInsensitiveMap(int size) 313 { 314 entries = new CIMEntry[Math.max(size, 3)]; 315 } 316 317 public CaseInsensitiveMap(Map<String, ? extends V> map) 318 { 319 this(map.size()); 320 321 for (Map.Entry<String, ? extends V> entry : map.entrySet()) 322 { 323 put(entry.getKey(), entry.getValue()); 324 } 325 } 326 327 @Override 328 public void clear() 329 { 330 for (int i = 0; i < size; i++) 331 entries[i] = null; 332 333 size = 0; 334 modCount++; 335 } 336 337 @Override 338 public boolean isEmpty() 339 { 340 return size == 0; 341 } 342 343 @Override 344 public int size() 345 { 346 return size; 347 } 348 349 @SuppressWarnings("unchecked") 350 @Override 351 public V put(String key, V value) 352 { 353 int hashCode = caseInsenitiveHashCode(key); 354 355 return select(key, hashCode).put(key, hashCode, value); 356 } 357 358 @Override 359 public boolean containsKey(Object key) 360 { 361 return select(key).isFound(); 362 } 363 364 @Override 365 public V get(Object key) 366 { 367 return select(key).get(); 368 } 369 370 @Override 371 public V remove(Object key) 372 { 373 return select(key).remove(); 374 } 375 376 @SuppressWarnings("unchecked") 377 @Override 378 public Set<Map.Entry<String, V>> entrySet() 379 { 380 if (entrySet == null) entrySet = new EntrySet(); 381 382 return entrySet; 383 } 384 385 private Position select(Object key) 386 { 387 if (key == null || key instanceof String) 388 { 389 String keyString = (String) key; 390 return select(keyString, caseInsenitiveHashCode(keyString)); 391 } 392 393 return new Position(0, false); 394 } 395 396 /** 397 * Searches the elements for the index of the indicated key and (case insensitive) hash code. Sets the _cursor and 398 * _found attributes. 399 */ 400 private Position select(String key, int hashCode) 401 { 402 if (size == 0) return new Position(0, false); 403 404 int low = 0; 405 int high = size - 1; 406 407 int cursor; 408 409 while (low <= high) 410 { 411 cursor = (low + high) >> 1; 412 413 CIMEntry e = entries[cursor]; 414 415 if (e.hashCode < hashCode) 416 { 417 low = cursor + 1; 418 continue; 419 } 420 421 if (e.hashCode > hashCode) 422 { 423 high = cursor - 1; 424 continue; 425 } 426 427 return tunePosition(key, hashCode, cursor); 428 } 429 430 return new Position(low, false); 431 } 432 433 /** 434 * select() has located a matching hashCode, but there's an outlying possibility that multiple keys share the same 435 * hashCode. Backup the cursor until we get to locate the initial hashCode match, then march forward until the key 436 * is located, or the hashCode stops matching. 437 * 438 * @param key 439 * @param hashCode 440 */ 441 private Position tunePosition(String key, int hashCode, int cursor) 442 { 443 boolean found = false; 444 445 while (cursor > 0) 446 { 447 if (entries[cursor - 1].hashCode != hashCode) break; 448 449 cursor--; 450 } 451 452 while (true) 453 { 454 if (entries[cursor].matches(key)) 455 { 456 found = true; 457 break; 458 } 459 460 // Advance to the next entry. 461 462 cursor++; 463 464 // If out of entries, 465 if (cursor >= size || entries[cursor].hashCode != hashCode) break; 466 } 467 468 return new Position(cursor, found); 469 } 470 471 static int caseInsenitiveHashCode(String input) 472 { 473 if (input == null) return NULL_HASH; 474 475 int length = input.length(); 476 int hash = 0; 477 478 // This should end up more or less equal to input.toLowerCase().hashCode(), unless String 479 // changes its implementation. Let's hope this is reasonably fast. 480 481 for (int i = 0; i < length; i++) 482 { 483 int ch = input.charAt(i); 484 485 int caselessCh = Character.toLowerCase(ch); 486 487 hash = 31 * hash + caselessCh; 488 } 489 490 return hash; 491 } 492 493 }