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}