LinkedCaseInsensitiveMap delegates to LinkedHashMap instead of extending it
Issue: SPR-15026
This commit is contained in:
parent
d3f97e3092
commit
8147c112f5
|
@ -16,10 +16,13 @@
|
||||||
|
|
||||||
package org.springframework.util;
|
package org.springframework.util;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link LinkedHashMap} variant that stores String keys in a case-insensitive
|
* {@link LinkedHashMap} variant that stores String keys in a case-insensitive
|
||||||
|
@ -34,9 +37,11 @@ import java.util.Map;
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
|
public class LinkedCaseInsensitiveMap<V> implements Map<String, V>, Serializable, Cloneable {
|
||||||
|
|
||||||
private Map<String, String> caseInsensitiveKeys;
|
private final LinkedHashMap<String, V> targetMap;
|
||||||
|
|
||||||
|
private final HashMap<String, String> caseInsensitiveKeys;
|
||||||
|
|
||||||
private final Locale locale;
|
private final Locale locale;
|
||||||
|
|
||||||
|
@ -46,7 +51,7 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
|
||||||
* @see java.lang.String#toLowerCase()
|
* @see java.lang.String#toLowerCase()
|
||||||
*/
|
*/
|
||||||
public LinkedCaseInsensitiveMap() {
|
public LinkedCaseInsensitiveMap() {
|
||||||
this(null);
|
this((Locale) null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,9 +61,7 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
|
||||||
* @see java.lang.String#toLowerCase(java.util.Locale)
|
* @see java.lang.String#toLowerCase(java.util.Locale)
|
||||||
*/
|
*/
|
||||||
public LinkedCaseInsensitiveMap(Locale locale) {
|
public LinkedCaseInsensitiveMap(Locale locale) {
|
||||||
super();
|
this(16, locale);
|
||||||
this.caseInsensitiveKeys = new HashMap<>();
|
|
||||||
this.locale = (locale != null ? locale : Locale.getDefault());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,19 +84,68 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
|
||||||
* @see java.lang.String#toLowerCase(java.util.Locale)
|
* @see java.lang.String#toLowerCase(java.util.Locale)
|
||||||
*/
|
*/
|
||||||
public LinkedCaseInsensitiveMap(int initialCapacity, Locale locale) {
|
public LinkedCaseInsensitiveMap(int initialCapacity, Locale locale) {
|
||||||
super(initialCapacity);
|
this.targetMap = new LinkedHashMap<String, V>(initialCapacity) {
|
||||||
|
@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.caseInsensitiveKeys = new HashMap<>(initialCapacity);
|
||||||
this.locale = (locale != null ? locale : Locale.getDefault());
|
this.locale = (locale != null ? locale : Locale.getDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return this.targetMap.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return this.targetMap.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsValue(Object value) {
|
||||||
|
return this.targetMap.containsValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
@Override
|
||||||
public V put(String key, V value) {
|
public V put(String key, V value) {
|
||||||
String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key);
|
String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key);
|
||||||
if (oldKey != null && !oldKey.equals(key)) {
|
if (oldKey != null && !oldKey.equals(key)) {
|
||||||
super.remove(oldKey);
|
this.targetMap.remove(oldKey);
|
||||||
}
|
}
|
||||||
return super.put(key, value);
|
return this.targetMap.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -116,19 +168,18 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
|
||||||
if (key instanceof String) {
|
if (key instanceof String) {
|
||||||
String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
|
String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
|
||||||
if (caseInsensitiveKey != null) {
|
if (caseInsensitiveKey != null) {
|
||||||
return super.get(caseInsensitiveKey);
|
return this.targetMap.get(caseInsensitiveKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overridden to avoid LinkedHashMap's own hash computation in its getOrDefault impl
|
|
||||||
@Override
|
@Override
|
||||||
public V getOrDefault(Object key, V defaultValue) {
|
public V getOrDefault(Object key, V defaultValue) {
|
||||||
if (key instanceof String) {
|
if (key instanceof String) {
|
||||||
String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
|
String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
|
||||||
if (caseInsensitiveKey != null) {
|
if (caseInsensitiveKey != null) {
|
||||||
return super.get(caseInsensitiveKey);
|
return this.targetMap.get(caseInsensitiveKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
@ -139,7 +190,7 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
|
||||||
if (key instanceof String) {
|
if (key instanceof String) {
|
||||||
String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key));
|
String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key));
|
||||||
if (caseInsensitiveKey != null) {
|
if (caseInsensitiveKey != null) {
|
||||||
return super.remove(caseInsensitiveKey);
|
return this.targetMap.remove(caseInsensitiveKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -148,15 +199,28 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public void clear() {
|
||||||
this.caseInsensitiveKeys.clear();
|
this.caseInsensitiveKeys.clear();
|
||||||
super.clear();
|
this.targetMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinkedCaseInsensitiveMap<V> clone() {
|
||||||
|
return new LinkedCaseInsensitiveMap<>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
public boolean equals(Object obj) {
|
||||||
public Object clone() {
|
return this.targetMap.equals(obj);
|
||||||
LinkedCaseInsensitiveMap<V> copy = (LinkedCaseInsensitiveMap<V>) super.clone();
|
}
|
||||||
copy.caseInsensitiveKeys = new HashMap<>(this.caseInsensitiveKeys);
|
|
||||||
return copy;
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.targetMap.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.targetMap.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -172,4 +236,14 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
|
||||||
return key.toLowerCase(this.locale);
|
return key.toLowerCase(this.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ import java.util.Set;
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
*/
|
*/
|
||||||
public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializable {
|
public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializable, Cloneable {
|
||||||
|
|
||||||
private static final long serialVersionUID = 3801124242820219131L;
|
private static final long serialVersionUID = 3801124242820219131L;
|
||||||
|
|
||||||
|
@ -176,18 +176,6 @@ public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializa
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a regular copy of this Map.
|
|
||||||
* @return a shallow copy of this Map, reusing this Map's value-holding List entries
|
|
||||||
* @since 4.2
|
|
||||||
* @see LinkedMultiValueMap#LinkedMultiValueMap(Map)
|
|
||||||
* @see #deepCopy()
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public LinkedMultiValueMap<K, V> clone() {
|
|
||||||
return new LinkedMultiValueMap<>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a deep copy of this Map.
|
* Create a deep copy of this Map.
|
||||||
* @return a copy of this Map, including a copy of each value-holding List entry
|
* @return a copy of this Map, including a copy of each value-holding List entry
|
||||||
|
@ -202,6 +190,17 @@ public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializa
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a regular copy of this Map.
|
||||||
|
* @return a shallow copy of this Map, reusing this Map's value-holding List entries
|
||||||
|
* @since 4.2
|
||||||
|
* @see LinkedMultiValueMap#LinkedMultiValueMap(Map)
|
||||||
|
* @see #deepCopy()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public LinkedMultiValueMap<K, V> clone() {
|
||||||
|
return new LinkedMultiValueMap<>(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
|
|
|
@ -74,11 +74,28 @@ public class LinkedCaseInsensitiveMapTests {
|
||||||
assertEquals("N", map.getOrDefault(new Object(), "N"));
|
assertEquals("N", map.getOrDefault(new Object(), "N"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void computeIfAbsentWithExistingValue() {
|
||||||
|
map.put("key", "value1");
|
||||||
|
map.put("KEY", "value2");
|
||||||
|
map.put("Key", "value3");
|
||||||
|
assertEquals("value3", map.computeIfAbsent("key", key -> "value1"));
|
||||||
|
assertEquals("value3", map.computeIfAbsent("KEY", key -> "value2"));
|
||||||
|
assertEquals("value3", map.computeIfAbsent("Key", key -> "value3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void computeIfAbsentWithComputedValue() {
|
||||||
|
assertEquals("value1", map.computeIfAbsent("key", key -> "value1"));
|
||||||
|
assertEquals("value1", map.computeIfAbsent("KEY", key -> "value2"));
|
||||||
|
assertEquals("value1", map.computeIfAbsent("Key", key -> "value3"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void mapClone() {
|
public void mapClone() {
|
||||||
map.put("key", "value1");
|
map.put("key", "value1");
|
||||||
LinkedCaseInsensitiveMap<String> copy = (LinkedCaseInsensitiveMap<String>) map.clone();
|
LinkedCaseInsensitiveMap<String> copy = map.clone();
|
||||||
assertEquals("value1", map.get("key"));
|
assertEquals("value1", map.get("key"));
|
||||||
assertEquals("value1", map.get("KEY"));
|
assertEquals("value1", map.get("KEY"));
|
||||||
assertEquals("value1", map.get("Key"));
|
assertEquals("value1", map.get("Key"));
|
||||||
|
|
Loading…
Reference in New Issue