001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.util; 018 019import java.io.Serializable; 020import java.util.AbstractCollection; 021import java.util.AbstractSet; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.LinkedHashMap; 026import java.util.Locale; 027import java.util.Map; 028import java.util.Set; 029import java.util.Spliterator; 030import java.util.function.Consumer; 031import java.util.function.Function; 032 033import org.springframework.lang.Nullable; 034 035/** 036 * {@link LinkedHashMap} variant that stores String keys in a case-insensitive 037 * manner, for example for key-based access in a results table. 038 * 039 * <p>Preserves the original order as well as the original casing of keys, 040 * while allowing for contains, get and remove calls with any case of key. 041 * 042 * <p>Does <i>not</i> support {@code null} keys. 043 * 044 * @author Juergen Hoeller 045 * @author Phillip Webb 046 * @since 3.0 047 * @param <V> the value type 048 */ 049@SuppressWarnings("serial") 050public class LinkedCaseInsensitiveMap<V> implements Map<String, V>, Serializable, Cloneable { 051 052 private final LinkedHashMap<String, V> targetMap; 053 054 private final HashMap<String, String> caseInsensitiveKeys; 055 056 private final Locale locale; 057 058 @Nullable 059 private transient volatile Set<String> keySet; 060 061 @Nullable 062 private transient volatile Collection<V> values; 063 064 @Nullable 065 private transient volatile Set<Entry<String, V>> entrySet; 066 067 068 /** 069 * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys 070 * according to the default Locale (by default in lower case). 071 * @see #convertKey(String) 072 */ 073 public LinkedCaseInsensitiveMap() { 074 this((Locale) null); 075 } 076 077 /** 078 * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys 079 * according to the given Locale (by default in lower case). 080 * @param locale the Locale to use for case-insensitive key conversion 081 * @see #convertKey(String) 082 */ 083 public LinkedCaseInsensitiveMap(@Nullable Locale locale) { 084 this(16, locale); 085 } 086 087 /** 088 * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} 089 * with the given initial capacity and stores case-insensitive keys 090 * according to the default Locale (by default in lower case). 091 * @param initialCapacity the initial capacity 092 * @see #convertKey(String) 093 */ 094 public LinkedCaseInsensitiveMap(int initialCapacity) { 095 this(initialCapacity, null); 096 } 097 098 /** 099 * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} 100 * with the given initial capacity and stores case-insensitive keys 101 * according to the given Locale (by default in lower case). 102 * @param initialCapacity the initial capacity 103 * @param locale the Locale to use for case-insensitive key conversion 104 * @see #convertKey(String) 105 */ 106 public LinkedCaseInsensitiveMap(int initialCapacity, @Nullable Locale locale) { 107 this.targetMap = new LinkedHashMap<String, V>(initialCapacity) { 108 @Override 109 public boolean containsKey(Object key) { 110 return LinkedCaseInsensitiveMap.this.containsKey(key); 111 } 112 @Override 113 protected boolean removeEldestEntry(Map.Entry<String, V> eldest) { 114 boolean doRemove = LinkedCaseInsensitiveMap.this.removeEldestEntry(eldest); 115 if (doRemove) { 116 removeCaseInsensitiveKey(eldest.getKey()); 117 } 118 return doRemove; 119 } 120 }; 121 this.caseInsensitiveKeys = new HashMap<>(initialCapacity); 122 this.locale = (locale != null ? locale : Locale.getDefault()); 123 } 124 125 /** 126 * Copy constructor. 127 */ 128 @SuppressWarnings("unchecked") 129 private LinkedCaseInsensitiveMap(LinkedCaseInsensitiveMap<V> other) { 130 this.targetMap = (LinkedHashMap<String, V>) other.targetMap.clone(); 131 this.caseInsensitiveKeys = (HashMap<String, String>) other.caseInsensitiveKeys.clone(); 132 this.locale = other.locale; 133 } 134 135 136 // Implementation of java.util.Map 137 138 @Override 139 public int size() { 140 return this.targetMap.size(); 141 } 142 143 @Override 144 public boolean isEmpty() { 145 return this.targetMap.isEmpty(); 146 } 147 148 @Override 149 public boolean containsKey(Object key) { 150 return (key instanceof String && this.caseInsensitiveKeys.containsKey(convertKey((String) key))); 151 } 152 153 @Override 154 public boolean containsValue(Object value) { 155 return this.targetMap.containsValue(value); 156 } 157 158 @Override 159 @Nullable 160 public V get(Object key) { 161 if (key instanceof String) { 162 String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); 163 if (caseInsensitiveKey != null) { 164 return this.targetMap.get(caseInsensitiveKey); 165 } 166 } 167 return null; 168 } 169 170 @Override 171 @Nullable 172 public V getOrDefault(Object key, V defaultValue) { 173 if (key instanceof String) { 174 String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); 175 if (caseInsensitiveKey != null) { 176 return this.targetMap.get(caseInsensitiveKey); 177 } 178 } 179 return defaultValue; 180 } 181 182 @Override 183 @Nullable 184 public V put(String key, @Nullable V value) { 185 String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key); 186 V oldKeyValue = null; 187 if (oldKey != null && !oldKey.equals(key)) { 188 oldKeyValue = this.targetMap.remove(oldKey); 189 } 190 V oldValue = this.targetMap.put(key, value); 191 return (oldKeyValue != null ? oldKeyValue : oldValue); 192 } 193 194 @Override 195 public void putAll(Map<? extends String, ? extends V> map) { 196 if (map.isEmpty()) { 197 return; 198 } 199 map.forEach(this::put); 200 } 201 202 @Override 203 @Nullable 204 public V putIfAbsent(String key, @Nullable V value) { 205 String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); 206 if (oldKey != null) { 207 return this.targetMap.get(oldKey); 208 } 209 return this.targetMap.putIfAbsent(key, value); 210 } 211 212 @Override 213 @Nullable 214 public V computeIfAbsent(String key, Function<? super String, ? extends V> mappingFunction) { 215 String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); 216 if (oldKey != null) { 217 return this.targetMap.get(oldKey); 218 } 219 return this.targetMap.computeIfAbsent(key, mappingFunction); 220 } 221 222 @Override 223 @Nullable 224 public V remove(Object key) { 225 if (key instanceof String) { 226 String caseInsensitiveKey = removeCaseInsensitiveKey((String) key); 227 if (caseInsensitiveKey != null) { 228 return this.targetMap.remove(caseInsensitiveKey); 229 } 230 } 231 return null; 232 } 233 234 @Override 235 public void clear() { 236 this.caseInsensitiveKeys.clear(); 237 this.targetMap.clear(); 238 } 239 240 @Override 241 public Set<String> keySet() { 242 Set<String> keySet = this.keySet; 243 if (keySet == null) { 244 keySet = new KeySet(this.targetMap.keySet()); 245 this.keySet = keySet; 246 } 247 return keySet; 248 } 249 250 @Override 251 public Collection<V> values() { 252 Collection<V> values = this.values; 253 if (values == null) { 254 values = new Values(this.targetMap.values()); 255 this.values = values; 256 } 257 return values; 258 } 259 260 @Override 261 public Set<Entry<String, V>> entrySet() { 262 Set<Entry<String, V>> entrySet = this.entrySet; 263 if (entrySet == null) { 264 entrySet = new EntrySet(this.targetMap.entrySet()); 265 this.entrySet = entrySet; 266 } 267 return entrySet; 268 } 269 270 @Override 271 public LinkedCaseInsensitiveMap<V> clone() { 272 return new LinkedCaseInsensitiveMap<>(this); 273 } 274 275 @Override 276 public boolean equals(@Nullable Object other) { 277 return (this == other || this.targetMap.equals(other)); 278 } 279 280 @Override 281 public int hashCode() { 282 return this.targetMap.hashCode(); 283 } 284 285 @Override 286 public String toString() { 287 return this.targetMap.toString(); 288 } 289 290 291 // Specific to LinkedCaseInsensitiveMap 292 293 /** 294 * Return the locale used by this {@code LinkedCaseInsensitiveMap}. 295 * Used for case-insensitive key conversion. 296 * @since 4.3.10 297 * @see #LinkedCaseInsensitiveMap(Locale) 298 * @see #convertKey(String) 299 */ 300 public Locale getLocale() { 301 return this.locale; 302 } 303 304 /** 305 * Convert the given key to a case-insensitive key. 306 * <p>The default implementation converts the key 307 * to lower-case according to this Map's Locale. 308 * @param key the user-specified key 309 * @return the key to use for storing 310 * @see String#toLowerCase(Locale) 311 */ 312 protected String convertKey(String key) { 313 return key.toLowerCase(getLocale()); 314 } 315 316 /** 317 * Determine whether this map should remove the given eldest entry. 318 * @param eldest the candidate entry 319 * @return {@code true} for removing it, {@code false} for keeping it 320 * @see LinkedHashMap#removeEldestEntry 321 */ 322 protected boolean removeEldestEntry(Map.Entry<String, V> eldest) { 323 return false; 324 } 325 326 @Nullable 327 private String removeCaseInsensitiveKey(String key) { 328 return this.caseInsensitiveKeys.remove(convertKey(key)); 329 } 330 331 332 private class KeySet extends AbstractSet<String> { 333 334 private final Set<String> delegate; 335 336 KeySet(Set<String> delegate) { 337 this.delegate = delegate; 338 } 339 340 @Override 341 public int size() { 342 return this.delegate.size(); 343 } 344 345 @Override 346 public boolean contains(Object o) { 347 return this.delegate.contains(o); 348 } 349 350 @Override 351 public Iterator<String> iterator() { 352 return new KeySetIterator(); 353 } 354 355 @Override 356 public boolean remove(Object o) { 357 return LinkedCaseInsensitiveMap.this.remove(o) != null; 358 } 359 360 @Override 361 public void clear() { 362 LinkedCaseInsensitiveMap.this.clear(); 363 } 364 365 @Override 366 public Spliterator<String> spliterator() { 367 return this.delegate.spliterator(); 368 } 369 370 @Override 371 public void forEach(Consumer<? super String> action) { 372 this.delegate.forEach(action); 373 } 374 } 375 376 377 private class Values extends AbstractCollection<V> { 378 379 private final Collection<V> delegate; 380 381 Values(Collection<V> delegate) { 382 this.delegate = delegate; 383 } 384 385 @Override 386 public int size() { 387 return this.delegate.size(); 388 } 389 390 @Override 391 public boolean contains(Object o) { 392 return this.delegate.contains(o); 393 } 394 395 @Override 396 public Iterator<V> iterator() { 397 return new ValuesIterator(); 398 } 399 400 @Override 401 public void clear() { 402 LinkedCaseInsensitiveMap.this.clear(); 403 } 404 405 @Override 406 public Spliterator<V> spliterator() { 407 return this.delegate.spliterator(); 408 } 409 410 @Override 411 public void forEach(Consumer<? super V> action) { 412 this.delegate.forEach(action); 413 } 414 } 415 416 417 private class EntrySet extends AbstractSet<Entry<String, V>> { 418 419 private final Set<Entry<String, V>> delegate; 420 421 public EntrySet(Set<Entry<String, V>> delegate) { 422 this.delegate = delegate; 423 } 424 425 @Override 426 public int size() { 427 return this.delegate.size(); 428 } 429 430 @Override 431 public boolean contains(Object o) { 432 return this.delegate.contains(o); 433 } 434 435 @Override 436 public Iterator<Entry<String, V>> iterator() { 437 return new EntrySetIterator(); 438 } 439 440 @Override 441 @SuppressWarnings("unchecked") 442 public boolean remove(Object o) { 443 if (this.delegate.remove(o)) { 444 removeCaseInsensitiveKey(((Map.Entry<String, V>) o).getKey()); 445 return true; 446 } 447 return false; 448 } 449 450 @Override 451 public void clear() { 452 this.delegate.clear(); 453 caseInsensitiveKeys.clear(); 454 } 455 456 @Override 457 public Spliterator<Entry<String, V>> spliterator() { 458 return this.delegate.spliterator(); 459 } 460 461 @Override 462 public void forEach(Consumer<? super Entry<String, V>> action) { 463 this.delegate.forEach(action); 464 } 465 } 466 467 468 private abstract class EntryIterator<T> implements Iterator<T> { 469 470 private final Iterator<Entry<String, V>> delegate; 471 472 @Nullable 473 private Entry<String, V> last; 474 475 public EntryIterator() { 476 this.delegate = targetMap.entrySet().iterator(); 477 } 478 479 protected Entry<String, V> nextEntry() { 480 Entry<String, V> entry = this.delegate.next(); 481 this.last = entry; 482 return entry; 483 } 484 485 @Override 486 public boolean hasNext() { 487 return this.delegate.hasNext(); 488 } 489 490 @Override 491 public void remove() { 492 this.delegate.remove(); 493 if (this.last != null) { 494 removeCaseInsensitiveKey(this.last.getKey()); 495 this.last = null; 496 } 497 } 498 } 499 500 501 private class KeySetIterator extends EntryIterator<String> { 502 503 @Override 504 public String next() { 505 return nextEntry().getKey(); 506 } 507 } 508 509 510 private class ValuesIterator extends EntryIterator<V> { 511 512 @Override 513 public V next() { 514 return nextEntry().getValue(); 515 } 516 } 517 518 519 private class EntrySetIterator extends EntryIterator<Entry<String, V>> { 520 521 @Override 522 public Entry<String, V> next() { 523 return nextEntry(); 524 } 525 } 526 527}