/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.util;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import org.springframework.lang.Nullable;

LinkedHashMap variant that stores String keys in a case-insensitive manner, for example for key-based access in a results table.

Preserves the original order as well as the original casing of keys, while allowing for contains, get and remove calls with any case of key.

Does not support null keys.

Author:Juergen Hoeller
Type parameters:
  • <V> – the value type
Since:3.0
/** * {@link LinkedHashMap} variant that stores String keys in a case-insensitive * manner, for example for key-based access in a results table. * * <p>Preserves the original order as well as the original casing of keys, * while allowing for contains, get and remove calls with any case of key. * * <p>Does <i>not</i> support {@code null} keys. * * @author Juergen Hoeller * @since 3.0 * @param <V> the value type */
@SuppressWarnings("serial") public class LinkedCaseInsensitiveMap<V> implements Map<String, V>, Serializable, Cloneable { private final LinkedHashMap<String, V> targetMap; private final HashMap<String, String> caseInsensitiveKeys; private final Locale locale;
Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys according to the default Locale (by default in lower case).
See Also:
  • convertKey(String)
/** * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys * according to the default Locale (by default in lower case). * @see #convertKey(String) */
public LinkedCaseInsensitiveMap() { this((Locale) null); }
Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys according to the given Locale (by default in lower case).
Params:
  • locale – the Locale to use for case-insensitive key conversion
See Also:
/** * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys * according to the given Locale (by default in lower case). * @param locale the Locale to use for case-insensitive key conversion * @see #convertKey(String) */
public LinkedCaseInsensitiveMap(@Nullable Locale locale) { this(16, locale); }
Create a new LinkedCaseInsensitiveMap that wraps a LinkedHashMap with the given initial capacity and stores case-insensitive keys according to the default Locale (by default in lower case).
Params:
  • initialCapacity – the initial capacity
See Also:
/** * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} * with the given initial capacity and stores case-insensitive keys * according to the default Locale (by default in lower case). * @param initialCapacity the initial capacity * @see #convertKey(String) */
public LinkedCaseInsensitiveMap(int initialCapacity) { this(initialCapacity, null); }
Create a new LinkedCaseInsensitiveMap that wraps a LinkedHashMap with the given initial capacity and stores case-insensitive keys according to the given Locale (by default in lower case).
Params:
  • initialCapacity – the initial capacity
  • locale – the Locale to use for case-insensitive key conversion
See Also:
/** * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} * with the given initial capacity and stores case-insensitive keys * according to the given Locale (by default in lower case). * @param initialCapacity the initial capacity * @param locale the Locale to use for case-insensitive key conversion * @see #convertKey(String) */
public LinkedCaseInsensitiveMap(int initialCapacity, @Nullable Locale locale) { this.targetMap = new LinkedHashMap<String, V>(initialCapacity) { @Override public boolean containsKey(Object key) { return LinkedCaseInsensitiveMap.this.containsKey(key); } @Override protected boolean removeEldestEntry(Map.Entry<String, V> eldest) { boolean doRemove = LinkedCaseInsensitiveMap.this.removeEldestEntry(eldest); if (doRemove) { caseInsensitiveKeys.remove(convertKey(eldest.getKey())); } return doRemove; } }; this.caseInsensitiveKeys = new HashMap<>(initialCapacity); this.locale = (locale != null ? locale : Locale.getDefault()); }
Copy constructor.
/** * Copy constructor. */
@SuppressWarnings("unchecked") private LinkedCaseInsensitiveMap(LinkedCaseInsensitiveMap<V> other) { this.targetMap = (LinkedHashMap<String, V>) other.targetMap.clone(); this.caseInsensitiveKeys = (HashMap<String, String>) other.caseInsensitiveKeys.clone(); this.locale = other.locale; } // Implementation of java.util.Map @Override public int size() { return this.targetMap.size(); } @Override public boolean isEmpty() { return this.targetMap.isEmpty(); } @Override public boolean containsKey(Object key) { return (key instanceof String && this.caseInsensitiveKeys.containsKey(convertKey((String) key))); } @Override public boolean containsValue(Object value) { return this.targetMap.containsValue(value); } @Override @Nullable public V get(Object key) { if (key instanceof String) { String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); if (caseInsensitiveKey != null) { return this.targetMap.get(caseInsensitiveKey); } } return null; } @Override @Nullable public V getOrDefault(Object key, V defaultValue) { if (key instanceof String) { String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); if (caseInsensitiveKey != null) { return this.targetMap.get(caseInsensitiveKey); } } return defaultValue; } @Override @Nullable public V put(String key, @Nullable V value) { String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key); V oldKeyValue = null; if (oldKey != null && !oldKey.equals(key)) { oldKeyValue = this.targetMap.remove(oldKey); } V oldValue = this.targetMap.put(key, value); return (oldKeyValue != null ? oldKeyValue : oldValue); } @Override public void putAll(Map<? extends String, ? extends V> map) { if (map.isEmpty()) { return; } map.forEach(this::put); } @Override @Nullable public V putIfAbsent(String key, @Nullable V value) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { return this.targetMap.get(oldKey); } return this.targetMap.putIfAbsent(key, value); } @Override @Nullable public V computeIfAbsent(String key, Function<? super String, ? extends V> mappingFunction) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { return this.targetMap.get(oldKey); } return this.targetMap.computeIfAbsent(key, mappingFunction); } @Override @Nullable public V remove(Object key) { if (key instanceof String) { String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key)); if (caseInsensitiveKey != null) { return this.targetMap.remove(caseInsensitiveKey); } } return null; } @Override public void clear() { this.caseInsensitiveKeys.clear(); this.targetMap.clear(); } @Override public Set<String> keySet() { return this.targetMap.keySet(); } @Override public Collection<V> values() { return this.targetMap.values(); } @Override public Set<Entry<String, V>> entrySet() { return this.targetMap.entrySet(); } @Override public LinkedCaseInsensitiveMap<V> clone() { return new LinkedCaseInsensitiveMap<>(this); } @Override public boolean equals(Object obj) { return this.targetMap.equals(obj); } @Override public int hashCode() { return this.targetMap.hashCode(); } @Override public String toString() { return this.targetMap.toString(); } // Specific to LinkedCaseInsensitiveMap
Return the locale used by this LinkedCaseInsensitiveMap. Used for case-insensitive key conversion.
See Also:
Since:4.3.10
/** * Return the locale used by this {@code LinkedCaseInsensitiveMap}. * Used for case-insensitive key conversion. * @since 4.3.10 * @see #LinkedCaseInsensitiveMap(Locale) * @see #convertKey(String) */
public Locale getLocale() { return this.locale; }
Convert the given key to a case-insensitive key.

The default implementation converts the key to lower-case according to this Map's Locale.

Params:
  • key – the user-specified key
See Also:
Returns:the key to use for storing
/** * Convert the given key to a case-insensitive key. * <p>The default implementation converts the key * to lower-case according to this Map's Locale. * @param key the user-specified key * @return the key to use for storing * @see String#toLowerCase(Locale) */
protected String convertKey(String key) { return key.toLowerCase(getLocale()); }
Determine whether this map should remove the given eldest entry.
Params:
  • eldest – the candidate entry
See Also:
Returns:true for removing it, false for keeping it
/** * Determine whether this map should remove the given eldest entry. * @param eldest the candidate entry * @return {@code true} for removing it, {@code false} for keeping it * @see LinkedHashMap#removeEldestEntry */
protected boolean removeEldestEntry(Map.Entry<String, V> eldest) { return false; } }