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.Collection; 021import java.util.HashMap; 022import java.util.LinkedHashMap; 023import java.util.Locale; 024import java.util.Map; 025import java.util.Set; 026 027/** 028 * {@link LinkedHashMap} variant that stores String keys in a case-insensitive 029 * manner, for example for key-based access in a results table. 030 * 031 * <p>Preserves the original order as well as the original casing of keys, 032 * while allowing for contains, get and remove calls with any case of key. 033 * 034 * <p>Does <i>not</i> support {@code null} keys. 035 * 036 * @author Juergen Hoeller 037 * @since 3.0 038 * @param <V> the value type 039 */ 040@SuppressWarnings("serial") 041public class LinkedCaseInsensitiveMap<V> implements Map<String, V>, Serializable, Cloneable { 042 043 private final LinkedHashMap<String, V> targetMap; 044 045 private final HashMap<String, String> caseInsensitiveKeys; 046 047 private final Locale locale; 048 049 050 /** 051 * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys 052 * according to the default Locale (by default in lower case). 053 * @see #convertKey(String) 054 */ 055 public LinkedCaseInsensitiveMap() { 056 this((Locale) null); 057 } 058 059 /** 060 * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys 061 * according to the given Locale (by default in lower case). 062 * @param locale the Locale to use for case-insensitive key conversion 063 * @see #convertKey(String) 064 */ 065 public LinkedCaseInsensitiveMap(Locale locale) { 066 this(16, locale); 067 } 068 069 /** 070 * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} 071 * with the given initial capacity and stores case-insensitive keys 072 * according to the default Locale (by default in lower case). 073 * @param initialCapacity the initial capacity 074 * @see #convertKey(String) 075 */ 076 public LinkedCaseInsensitiveMap(int initialCapacity) { 077 this(initialCapacity, null); 078 } 079 080 /** 081 * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} 082 * with the given initial capacity and stores case-insensitive keys 083 * according to the given Locale (by default in lower case). 084 * @param initialCapacity the initial capacity 085 * @param locale the Locale to use for case-insensitive key conversion 086 * @see #convertKey(String) 087 */ 088 public LinkedCaseInsensitiveMap(int initialCapacity, Locale locale) { 089 this.targetMap = new LinkedHashMap<String, V>(initialCapacity) { 090 @Override 091 public boolean containsKey(Object key) { 092 return LinkedCaseInsensitiveMap.this.containsKey(key); 093 } 094 @Override 095 protected boolean removeEldestEntry(Map.Entry<String, V> eldest) { 096 boolean doRemove = LinkedCaseInsensitiveMap.this.removeEldestEntry(eldest); 097 if (doRemove) { 098 caseInsensitiveKeys.remove(convertKey(eldest.getKey())); 099 } 100 return doRemove; 101 } 102 }; 103 this.caseInsensitiveKeys = new HashMap<String, String>(initialCapacity); 104 this.locale = (locale != null ? locale : Locale.getDefault()); 105 } 106 107 /** 108 * Copy constructor. 109 */ 110 @SuppressWarnings("unchecked") 111 private LinkedCaseInsensitiveMap(LinkedCaseInsensitiveMap<V> other) { 112 this.targetMap = (LinkedHashMap<String, V>) other.targetMap.clone(); 113 this.caseInsensitiveKeys = (HashMap<String, String>) other.caseInsensitiveKeys.clone(); 114 this.locale = other.locale; 115 } 116 117 118 // Implementation of java.util.Map 119 120 @Override 121 public int size() { 122 return this.targetMap.size(); 123 } 124 125 @Override 126 public boolean isEmpty() { 127 return this.targetMap.isEmpty(); 128 } 129 130 @Override 131 public boolean containsKey(Object key) { 132 return (key instanceof String && this.caseInsensitiveKeys.containsKey(convertKey((String) key))); 133 } 134 135 @Override 136 public boolean containsValue(Object value) { 137 return this.targetMap.containsValue(value); 138 } 139 140 @Override 141 public V get(Object key) { 142 if (key instanceof String) { 143 String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); 144 if (caseInsensitiveKey != null) { 145 return this.targetMap.get(caseInsensitiveKey); 146 } 147 } 148 return null; 149 } 150 151 @Override 152 public V getOrDefault(Object key, V defaultValue) { 153 if (key instanceof String) { 154 String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); 155 if (caseInsensitiveKey != null) { 156 return this.targetMap.get(caseInsensitiveKey); 157 } 158 } 159 return defaultValue; 160 } 161 162 @Override 163 public V put(String key, V value) { 164 String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key); 165 if (oldKey != null && !oldKey.equals(key)) { 166 this.targetMap.remove(oldKey); 167 } 168 return this.targetMap.put(key, value); 169 } 170 171 @Override 172 public void putAll(Map<? extends String, ? extends V> map) { 173 if (map.isEmpty()) { 174 return; 175 } 176 for (Map.Entry<? extends String, ? extends V> entry : map.entrySet()) { 177 put(entry.getKey(), entry.getValue()); 178 } 179 } 180 181 @Override 182 public V remove(Object key) { 183 if (key instanceof String) { 184 String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key)); 185 if (caseInsensitiveKey != null) { 186 return this.targetMap.remove(caseInsensitiveKey); 187 } 188 } 189 return null; 190 } 191 192 @Override 193 public void clear() { 194 this.caseInsensitiveKeys.clear(); 195 this.targetMap.clear(); 196 } 197 198 @Override 199 public Set<String> keySet() { 200 return this.targetMap.keySet(); 201 } 202 203 @Override 204 public Collection<V> values() { 205 return this.targetMap.values(); 206 } 207 208 @Override 209 public Set<Entry<String, V>> entrySet() { 210 return this.targetMap.entrySet(); 211 } 212 213 @Override 214 public LinkedCaseInsensitiveMap<V> clone() { 215 return new LinkedCaseInsensitiveMap<V>(this); 216 } 217 218 @Override 219 public boolean equals(Object other) { 220 return (this == other || this.targetMap.equals(other)); 221 } 222 223 @Override 224 public int hashCode() { 225 return this.targetMap.hashCode(); 226 } 227 228 @Override 229 public String toString() { 230 return this.targetMap.toString(); 231 } 232 233 234 // Specific to LinkedCaseInsensitiveMap 235 236 /** 237 * Return the locale used by this {@code LinkedCaseInsensitiveMap}. 238 * Used for case-insensitive key conversion. 239 * @since 4.3.10 240 * @see #LinkedCaseInsensitiveMap(Locale) 241 * @see #convertKey(String) 242 */ 243 public Locale getLocale() { 244 return this.locale; 245 } 246 247 /** 248 * Convert the given key to a case-insensitive key. 249 * <p>The default implementation converts the key 250 * to lower-case according to this Map's Locale. 251 * @param key the user-specified key 252 * @return the key to use for storing 253 * @see String#toLowerCase(Locale) 254 */ 255 protected String convertKey(String key) { 256 return key.toLowerCase(getLocale()); 257 } 258 259 /** 260 * Determine whether this map should remove the given eldest entry. 261 * @param eldest the candidate entry 262 * @return {@code true} for removing it, {@code false} for keeping it 263 * @see LinkedHashMap#removeEldestEntry 264 */ 265 protected boolean removeEldestEntry(Map.Entry<String, V> eldest) { 266 return false; 267 } 268 269}