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 015package org.apache.tapestry5.commons.util; 016 017import java.io.Serializable; 018import 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 */ 030public class CaseInsensitiveMap<V> extends AbstractMap<String, V> implements Serializable 031{ 032 033 private static final long serialVersionUID = -3162531976817110908L; 034 035 private static final int NULL_HASH = Integer.MIN_VALUE; 036 037 private static final int DEFAULT_SIZE = 20; 038 039 private static class CIMEntry<V> implements Map.Entry<String, V>, Serializable 040 { 041 private static final long serialVersionUID = 6713986085221148350L; 042 043 private String key; 044 045 private final int hashCode; 046 047 V value; 048 049 public CIMEntry(final String key, final int hashCode, V value) 050 { 051 this.key = key; 052 this.hashCode = hashCode; 053 this.value = value; 054 } 055 056 @Override 057 public String getKey() 058 { 059 return key; 060 } 061 062 @Override 063 public V getValue() 064 { 065 return value; 066 } 067 068 @Override 069 public V setValue(V value) 070 { 071 V result = this.value; 072 073 this.value = value; 074 075 return result; 076 } 077 078 /** 079 * Returns true if both keys are null, or if the provided key is the same as, or case-insensitively equal to, 080 * the entrie's key. 081 * 082 * @param key to compare against 083 * @return true if equal 084 */ 085 @SuppressWarnings({ "StringEquality" }) 086 boolean matches(String key) 087 { 088 return key == this.key || (key != null && key.equalsIgnoreCase(this.key)); 089 } 090 091 boolean valueMatches(Object value) 092 { 093 return value == this.value || (value != null && value.equals(this.value)); 094 } 095 096 @Override 097 public String toString() { 098 StringBuilder builder = new StringBuilder(); 099 builder.append("CIMEntry [key=").append(key).append(", value=").append(value).append(']'); 100 return builder.toString(); 101 } 102 103 } 104 105 private class EntrySetIterator implements Iterator<Entry<String, V>> 106 { 107 int expectedModCount = modCount; 108 109 int index; 110 111 int current = -1; 112 113 @Override 114 public boolean hasNext() 115 { 116 return index < size; 117 } 118 119 @Override 120 public Entry<String, V> next() 121 { 122 check(); 123 124 if (index >= size) throw new NoSuchElementException(); 125 126 current = index++; 127 128 return entries[current]; 129 } 130 131 @Override 132 public void remove() 133 { 134 check(); 135 136 if (current < 0) throw new NoSuchElementException(); 137 138 new Position(current, true).remove(); 139 140 expectedModCount = modCount; 141 } 142 143 private void check() 144 { 145 if (expectedModCount != modCount) throw new ConcurrentModificationException(); 146 } 147 148 @Override 149 public String toString() { 150 return "EntrySetIterator, current key: "+entries[current].key; 151 } 152 } 153 154 private class EntrySet extends AbstractSet<Entry<String, V>> 155 { 156 @Override 157 public Iterator<Entry<String, V>> iterator() 158 { 159 return new EntrySetIterator(); 160 } 161 162 @Override 163 public int size() 164 { 165 return size; 166 } 167 168 @Override 169 public void clear() 170 { 171 CaseInsensitiveMap.this.clear(); 172 } 173 174 @Override 175 public boolean contains(Object o) 176 { 177 if (!(o instanceof Map.Entry)) return false; 178 179 @SuppressWarnings("rawtypes") 180 Map.Entry e = (Map.Entry) o; 181 182 Position position = select(e.getKey()); 183 184 return position.isFound() && position.entry().valueMatches(e.getValue()); 185 } 186 187 @Override 188 public boolean remove(Object o) 189 { 190 if (!(o instanceof Map.Entry)) return false; 191 192 @SuppressWarnings("rawtypes") 193 Map.Entry e = (Map.Entry) o; 194 195 Position position = select(e.getKey()); 196 197 if (position.isFound() && position.entry().valueMatches(e.getValue())) 198 { 199 position.remove(); 200 return true; 201 } 202 203 return false; 204 } 205 206 } 207 208 private class Position 209 { 210 private final int cursor; 211 212 private final boolean found; 213 214 Position(int cursor, boolean found) 215 { 216 this.cursor = cursor; 217 this.found = found; 218 } 219 220 boolean isFound() 221 { 222 return found; 223 } 224 225 CIMEntry<V> entry() 226 { 227 return entries[cursor]; 228 } 229 230 V get() 231 { 232 return found ? entries[cursor].value : null; 233 } 234 235 V remove() 236 { 237 if (!found) return null; 238 239 V result = entries[cursor].value; 240 241 // Remove the entry by shifting everything else down. 242 243 System.arraycopy(entries, cursor + 1, entries, cursor, size - cursor - 1); 244 245 // We shifted down, leaving one (now duplicate) entry behind. 246 247 entries[--size] = null; 248 249 // A structural change for sure 250 251 modCount++; 252 253 return result; 254 } 255 256 @SuppressWarnings("unchecked") 257 V put(String key, int hashCode, V newValue) 258 { 259 if (found) 260 { 261 CIMEntry<V> e = entries[cursor]; 262 263 V result = e.value; 264 265 // Not a structural change, so no change to modCount 266 267 // Update the key (to maintain case). By definition, the hash code 268 // will not change. 269 270 e.key = key; 271 e.value = newValue; 272 273 return result; 274 } 275 276 // Not found, we're going to add it. 277 278 int newSize = size + 1; 279 280 try 281 { 282 if (newSize == entries.length) 283 { 284 // Time to expand! 285 286 int newCapacity = (size * 3) / 2 + 1; 287 288 CIMEntry<V>[] newEntries = new CIMEntry[newCapacity]; 289 290 System.arraycopy(entries, 0, newEntries, 0, cursor); 291 292 System.arraycopy(entries, cursor, newEntries, cursor + 1, size - cursor); 293 294 entries = newEntries; 295 } 296 else 297 { 298 // Open up a space for the new entry 299 System.arraycopy(entries, cursor, entries, cursor + 1, size - cursor); 300 } 301 } catch (ArrayIndexOutOfBoundsException e) 302 { 303 // TAP5-2526 304 throw new ConcurrentModificationException(); 305 } 306 307 CIMEntry<V> newEntry = new CIMEntry<V>(key, hashCode, newValue); 308 entries[cursor] = newEntry; 309 310 size++; 311 312 // This is definitely a structural change 313 314 modCount++; 315 316 return null; 317 } 318 319 } 320 321 // The list of entries. This is kept sorted by hash code. In some cases, there may be different 322 // keys with the same hash code in adjacent indexes. 323 private CIMEntry<V>[] entries; 324 325 private int size = 0; 326 327 // Used by iterators to check for concurrent modifications 328 329 private transient int modCount = 0; 330 331 private transient Set<Map.Entry<String, V>> entrySet; 332 333 public CaseInsensitiveMap() 334 { 335 this(DEFAULT_SIZE); 336 } 337 338 @SuppressWarnings("unchecked") 339 public CaseInsensitiveMap(int size) 340 { 341 entries = new CIMEntry[Math.max(size, 3)]; 342 } 343 344 public CaseInsensitiveMap(Map<String, ? extends V> map) 345 { 346 this(map.size()); 347 348 for (Map.Entry<String, ? extends V> entry : map.entrySet()) 349 { 350 put(entry.getKey(), entry.getValue()); 351 } 352 } 353 354 @Override 355 public void clear() 356 { 357 for (int i = 0; i < size; i++) 358 entries[i] = null; 359 360 size = 0; 361 modCount++; 362 } 363 364 @Override 365 public boolean isEmpty() 366 { 367 return size == 0; 368 } 369 370 @Override 371 public int size() 372 { 373 return size; 374 } 375 376 @Override 377 public V put(String key, V value) 378 { 379 int hashCode = caseInsenitiveHashCode(key); 380 381 return select(key, hashCode).put(key, hashCode, value); 382 } 383 384 @Override 385 public boolean containsKey(Object key) 386 { 387 return select(key).isFound(); 388 } 389 390 @Override 391 public V get(Object key) 392 { 393 return select(key).get(); 394 } 395 396 @Override 397 public V remove(Object key) 398 { 399 return select(key).remove(); 400 } 401 402 @Override 403 public Set<Map.Entry<String, V>> entrySet() 404 { 405 if (entrySet == null) entrySet = new EntrySet(); 406 407 return entrySet; 408 } 409 410 @Override 411 public Set<String> keySet() { 412 413 AbstractSet<String> set = new AbstractSet<String>() { 414 415 @Override 416 public Iterator<String> iterator() { 417 418 final Iterator<java.util.Map.Entry<String, V>> entrySetIterator = entrySet().iterator(); 419 420 return new Iterator<String>() { 421 @Override 422 public boolean hasNext() { 423 return entrySetIterator.hasNext(); 424 } 425 @Override 426 public String next() { 427 String nextKey = entrySetIterator.next().getKey(); 428 return nextKey; 429 } 430 431 @Override 432 public void remove() { 433 throw new UnsupportedOperationException("Modifications to the key set are not allowed."); 434 } 435 }; 436 } 437 438 @Override 439 public boolean contains(Object o) { 440 return containsKey(o); 441 } 442 443 @Override 444 public int size() { 445 return size; 446 } 447 }; 448 449 return Collections.unmodifiableSet(set); 450 } 451 452 private Position select(Object key) 453 { 454 if (key == null || key instanceof String) 455 { 456 String keyString = (String) key; 457 return select(keyString, caseInsenitiveHashCode(keyString)); 458 } 459 460 return new Position(0, false); 461 } 462 463 /** 464 * Searches the elements for the index of the indicated key and (case insensitive) hash code. Sets the _cursor and 465 * _found attributes. 466 */ 467 private Position select(String key, int hashCode) 468 { 469 if (size == 0) return new Position(0, false); 470 471 int low = 0; 472 int high = size - 1; 473 474 int cursor; 475 476 try 477 { 478 while (low <= high) 479 { 480 cursor = (low + high) >> 1; 481 482 CIMEntry<V> e = entries[cursor]; 483 484 if (e == null) 485 { 486 // TAP5-2520 487 throw new ConcurrentModificationException(); 488 } 489 490 if (e.hashCode < hashCode) 491 { 492 low = cursor + 1; 493 continue; 494 } 495 496 if (e.hashCode > hashCode) 497 { 498 high = cursor - 1; 499 continue; 500 } 501 502 return tunePosition(key, hashCode, cursor); 503 } 504 } catch (ArrayIndexOutOfBoundsException e) 505 { 506 // TAP5-2526 507 throw new ConcurrentModificationException(); 508 } 509 510 return new Position(low, false); 511 } 512 513 /** 514 * select() has located a matching hashCode, but there's an outlying possibility that multiple keys share the same 515 * hashCode. Backup the cursor until we get to locate the initial hashCode match, then march forward until the key 516 * is located, or the hashCode stops matching. 517 * 518 * @param key 519 * @param hashCode 520 */ 521 private Position tunePosition(String key, int hashCode, int cursor) 522 { 523 boolean found = false; 524 525 while (cursor > 0) 526 { 527 if (entries[cursor - 1].hashCode != hashCode) break; 528 529 cursor--; 530 } 531 532 while (true) 533 { 534 if (entries[cursor].matches(key)) 535 { 536 found = true; 537 break; 538 } 539 540 // Advance to the next entry. 541 542 cursor++; 543 544 // If out of entries, 545 if (cursor >= size || entries[cursor].hashCode != hashCode) break; 546 } 547 548 return new Position(cursor, found); 549 } 550 551 static int caseInsenitiveHashCode(String input) 552 { 553 if (input == null) return NULL_HASH; 554 555 int length = input.length(); 556 int hash = 0; 557 558 // This should end up more or less equal to input.toLowerCase().hashCode(), unless String 559 // changes its implementation. Let's hope this is reasonably fast. 560 561 for (int i = 0; i < length; i++) 562 { 563 int ch = input.charAt(i); 564 565 int caselessCh = Character.toLowerCase(ch); 566 567 hash = 31 * hash + caselessCh; 568 } 569 570 return hash; 571 } 572 573}