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;
 | 
			
		||||
 | 
			
		||||
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<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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +51,7 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
 | 
			
		|||
	 * @see java.lang.String#toLowerCase()
 | 
			
		||||
	 */
 | 
			
		||||
	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)
 | 
			
		||||
	 */
 | 
			
		||||
	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<V> extends LinkedHashMap<String, V> {
 | 
			
		|||
	 * @see java.lang.String#toLowerCase(java.util.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.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
 | 
			
		||||
	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<V> extends LinkedHashMap<String, V> {
 | 
			
		|||
		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<V> extends LinkedHashMap<String, V> {
 | 
			
		|||
		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<V> extends LinkedHashMap<String, V> {
 | 
			
		|||
	@Override
 | 
			
		||||
	public void clear() {
 | 
			
		||||
		this.caseInsensitiveKeys.clear();
 | 
			
		||||
		super.clear();
 | 
			
		||||
		this.targetMap.clear();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public LinkedCaseInsensitiveMap<V> clone() {
 | 
			
		||||
		return new LinkedCaseInsensitiveMap<>(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	@SuppressWarnings("unchecked")
 | 
			
		||||
	public Object clone() {
 | 
			
		||||
		LinkedCaseInsensitiveMap<V> copy = (LinkedCaseInsensitiveMap<V>) super.clone();
 | 
			
		||||
		copy.caseInsensitiveKeys = new HashMap<>(this.caseInsensitiveKeys);
 | 
			
		||||
		return copy;
 | 
			
		||||
	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<V> extends LinkedHashMap<String, V> {
 | 
			
		|||
		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
 | 
			
		||||
 * @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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
	 * @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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 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
 | 
			
		||||
	public boolean equals(Object obj) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<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"));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue