diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java index 91e4b1f41d..480037581e 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -16,10 +16,13 @@ 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; /** * {@link LinkedHashMap} variant that stores String keys in a case-insensitive @@ -34,9 +37,11 @@ import java.util.Map; * @since 3.0 */ @SuppressWarnings("serial") -public class LinkedCaseInsensitiveMap extends LinkedHashMap { +public class LinkedCaseInsensitiveMap implements Map, Serializable, Cloneable { - private Map caseInsensitiveKeys; + private final LinkedHashMap targetMap; + + private final HashMap caseInsensitiveKeys; private final Locale locale; @@ -46,7 +51,7 @@ public class LinkedCaseInsensitiveMap extends LinkedHashMap { * @see java.lang.String#toLowerCase() */ public LinkedCaseInsensitiveMap() { - this(null); + this((Locale) null); } /** @@ -56,9 +61,7 @@ public class LinkedCaseInsensitiveMap extends LinkedHashMap { * @see java.lang.String#toLowerCase(java.util.Locale) */ public LinkedCaseInsensitiveMap(Locale locale) { - super(); - this.caseInsensitiveKeys = new HashMap<>(); - this.locale = (locale != null ? locale : Locale.getDefault()); + this(16, locale); } /** @@ -81,19 +84,68 @@ public class LinkedCaseInsensitiveMap extends LinkedHashMap { * @see java.lang.String#toLowerCase(java.util.Locale) */ public LinkedCaseInsensitiveMap(int initialCapacity, Locale locale) { - super(initialCapacity); + this.targetMap = new LinkedHashMap(initialCapacity) { + @Override + protected boolean removeEldestEntry(Map.Entry 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. + */ + @SuppressWarnings("unchecked") + private LinkedCaseInsensitiveMap(LinkedCaseInsensitiveMap other) { + this.targetMap = (LinkedHashMap) other.targetMap.clone(); + this.caseInsensitiveKeys = (HashMap) 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 keySet() { + return this.targetMap.keySet(); + } + + @Override + public Collection values() { + return this.targetMap.values(); + } + + @Override + public Set> entrySet() { + return this.targetMap.entrySet(); + } @Override public V put(String key, V value) { String oldKey = this.caseInsensitiveKeys.put(convertKey(key), 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 @@ -116,19 +168,18 @@ public class LinkedCaseInsensitiveMap extends LinkedHashMap { if (key instanceof String) { String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); if (caseInsensitiveKey != null) { - return super.get(caseInsensitiveKey); + return this.targetMap.get(caseInsensitiveKey); } } return null; } - // Overridden to avoid LinkedHashMap's own hash computation in its getOrDefault impl @Override public V getOrDefault(Object key, V defaultValue) { if (key instanceof String) { String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); if (caseInsensitiveKey != null) { - return super.get(caseInsensitiveKey); + return this.targetMap.get(caseInsensitiveKey); } } return defaultValue; @@ -139,7 +190,7 @@ public class LinkedCaseInsensitiveMap extends LinkedHashMap { if (key instanceof String) { String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key)); if (caseInsensitiveKey != null) { - return super.remove(caseInsensitiveKey); + return this.targetMap.remove(caseInsensitiveKey); } } return null; @@ -148,15 +199,28 @@ public class LinkedCaseInsensitiveMap extends LinkedHashMap { @Override public void clear() { this.caseInsensitiveKeys.clear(); - super.clear(); + this.targetMap.clear(); } + @Override - @SuppressWarnings("unchecked") - public Object clone() { - LinkedCaseInsensitiveMap copy = (LinkedCaseInsensitiveMap) super.clone(); - copy.caseInsensitiveKeys = new HashMap<>(this.caseInsensitiveKeys); - return copy; + public LinkedCaseInsensitiveMap 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(); } @@ -172,4 +236,14 @@ public class LinkedCaseInsensitiveMap extends LinkedHashMap { 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 eldest) { + return false; + } + } diff --git a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java index 6148e84cd4..acbbcafceb 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java @@ -35,7 +35,7 @@ import java.util.Set; * @author Juergen Hoeller * @since 3.0 */ -public class LinkedMultiValueMap implements MultiValueMap, Serializable { +public class LinkedMultiValueMap implements MultiValueMap, Serializable, Cloneable { private static final long serialVersionUID = 3801124242820219131L; @@ -176,18 +176,6 @@ public class LinkedMultiValueMap implements MultiValueMap, 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 clone() { - return new LinkedMultiValueMap<>(this); - } - /** * Create a deep copy of this Map. * @return a copy of this Map, including a copy of each value-holding List entry @@ -202,6 +190,17 @@ public class LinkedMultiValueMap implements MultiValueMap, Serializa 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 clone() { + return new LinkedMultiValueMap<>(this); + } @Override public boolean equals(Object obj) { diff --git a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java index 1b3f757bd2..d9bd2b2d4f 100644 --- a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java @@ -74,11 +74,28 @@ public class LinkedCaseInsensitiveMapTests { 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 @SuppressWarnings("unchecked") public void mapClone() { map.put("key", "value1"); - LinkedCaseInsensitiveMap copy = (LinkedCaseInsensitiveMap) map.clone(); + LinkedCaseInsensitiveMap copy = map.clone(); assertEquals("value1", map.get("key")); assertEquals("value1", map.get("KEY")); assertEquals("value1", map.get("Key"));