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.AbstractCollection;
021import java.util.AbstractSet;
022import java.util.Collection;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.LinkedHashMap;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Set;
029import java.util.Spliterator;
030import java.util.function.Consumer;
031import java.util.function.Function;
032
033import org.springframework.lang.Nullable;
034
035/**
036 * {@link LinkedHashMap} variant that stores String keys in a case-insensitive
037 * manner, for example for key-based access in a results table.
038 *
039 * <p>Preserves the original order as well as the original casing of keys,
040 * while allowing for contains, get and remove calls with any case of key.
041 *
042 * <p>Does <i>not</i> support {@code null} keys.
043 *
044 * @author Juergen Hoeller
045 * @author Phillip Webb
046 * @since 3.0
047 * @param <V> the value type
048 */
049@SuppressWarnings("serial")
050public class LinkedCaseInsensitiveMap<V> implements Map<String, V>, Serializable, Cloneable {
051
052        private final LinkedHashMap<String, V> targetMap;
053
054        private final HashMap<String, String> caseInsensitiveKeys;
055
056        private final Locale locale;
057
058        @Nullable
059        private transient volatile Set<String> keySet;
060
061        @Nullable
062        private transient volatile Collection<V> values;
063
064        @Nullable
065        private transient volatile Set<Entry<String, V>> entrySet;
066
067
068        /**
069         * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys
070         * according to the default Locale (by default in lower case).
071         * @see #convertKey(String)
072         */
073        public LinkedCaseInsensitiveMap() {
074                this((Locale) null);
075        }
076
077        /**
078         * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys
079         * according to the given Locale (by default in lower case).
080         * @param locale the Locale to use for case-insensitive key conversion
081         * @see #convertKey(String)
082         */
083        public LinkedCaseInsensitiveMap(@Nullable Locale locale) {
084                this(16, locale);
085        }
086
087        /**
088         * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap}
089         * with the given initial capacity and stores case-insensitive keys
090         * according to the default Locale (by default in lower case).
091         * @param initialCapacity the initial capacity
092         * @see #convertKey(String)
093         */
094        public LinkedCaseInsensitiveMap(int initialCapacity) {
095                this(initialCapacity, null);
096        }
097
098        /**
099         * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap}
100         * with the given initial capacity and stores case-insensitive keys
101         * according to the given Locale (by default in lower case).
102         * @param initialCapacity the initial capacity
103         * @param locale the Locale to use for case-insensitive key conversion
104         * @see #convertKey(String)
105         */
106        public LinkedCaseInsensitiveMap(int initialCapacity, @Nullable Locale locale) {
107                this.targetMap = new LinkedHashMap<String, V>(initialCapacity) {
108                        @Override
109                        public boolean containsKey(Object key) {
110                                return LinkedCaseInsensitiveMap.this.containsKey(key);
111                        }
112                        @Override
113                        protected boolean removeEldestEntry(Map.Entry<String, V> eldest) {
114                                boolean doRemove = LinkedCaseInsensitiveMap.this.removeEldestEntry(eldest);
115                                if (doRemove) {
116                                        removeCaseInsensitiveKey(eldest.getKey());
117                                }
118                                return doRemove;
119                        }
120                };
121                this.caseInsensitiveKeys = new HashMap<>(initialCapacity);
122                this.locale = (locale != null ? locale : Locale.getDefault());
123        }
124
125        /**
126         * Copy constructor.
127         */
128        @SuppressWarnings("unchecked")
129        private LinkedCaseInsensitiveMap(LinkedCaseInsensitiveMap<V> other) {
130                this.targetMap = (LinkedHashMap<String, V>) other.targetMap.clone();
131                this.caseInsensitiveKeys = (HashMap<String, String>) other.caseInsensitiveKeys.clone();
132                this.locale = other.locale;
133        }
134
135
136        // Implementation of java.util.Map
137
138        @Override
139        public int size() {
140                return this.targetMap.size();
141        }
142
143        @Override
144        public boolean isEmpty() {
145                return this.targetMap.isEmpty();
146        }
147
148        @Override
149        public boolean containsKey(Object key) {
150                return (key instanceof String && this.caseInsensitiveKeys.containsKey(convertKey((String) key)));
151        }
152
153        @Override
154        public boolean containsValue(Object value) {
155                return this.targetMap.containsValue(value);
156        }
157
158        @Override
159        @Nullable
160        public V get(Object key) {
161                if (key instanceof String) {
162                        String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
163                        if (caseInsensitiveKey != null) {
164                                return this.targetMap.get(caseInsensitiveKey);
165                        }
166                }
167                return null;
168        }
169
170        @Override
171        @Nullable
172        public V getOrDefault(Object key, V defaultValue) {
173                if (key instanceof String) {
174                        String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
175                        if (caseInsensitiveKey != null) {
176                                return this.targetMap.get(caseInsensitiveKey);
177                        }
178                }
179                return defaultValue;
180        }
181
182        @Override
183        @Nullable
184        public V put(String key, @Nullable V value) {
185                String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key);
186                V oldKeyValue = null;
187                if (oldKey != null && !oldKey.equals(key)) {
188                        oldKeyValue = this.targetMap.remove(oldKey);
189                }
190                V oldValue = this.targetMap.put(key, value);
191                return (oldKeyValue != null ? oldKeyValue : oldValue);
192        }
193
194        @Override
195        public void putAll(Map<? extends String, ? extends V> map) {
196                if (map.isEmpty()) {
197                        return;
198                }
199                map.forEach(this::put);
200        }
201
202        @Override
203        @Nullable
204        public V putIfAbsent(String key, @Nullable V value) {
205                String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key);
206                if (oldKey != null) {
207                        return this.targetMap.get(oldKey);
208                }
209                return this.targetMap.putIfAbsent(key, value);
210        }
211
212        @Override
213        @Nullable
214        public V computeIfAbsent(String key, Function<? super String, ? extends V> mappingFunction) {
215                String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key);
216                if (oldKey != null) {
217                        return this.targetMap.get(oldKey);
218                }
219                return this.targetMap.computeIfAbsent(key, mappingFunction);
220        }
221
222        @Override
223        @Nullable
224        public V remove(Object key) {
225                if (key instanceof String) {
226                        String caseInsensitiveKey = removeCaseInsensitiveKey((String) key);
227                        if (caseInsensitiveKey != null) {
228                                return this.targetMap.remove(caseInsensitiveKey);
229                        }
230                }
231                return null;
232        }
233
234        @Override
235        public void clear() {
236                this.caseInsensitiveKeys.clear();
237                this.targetMap.clear();
238        }
239
240        @Override
241        public Set<String> keySet() {
242                Set<String> keySet = this.keySet;
243                if (keySet == null) {
244                        keySet = new KeySet(this.targetMap.keySet());
245                        this.keySet = keySet;
246                }
247                return keySet;
248        }
249
250        @Override
251        public Collection<V> values() {
252                Collection<V> values = this.values;
253                if (values == null) {
254                        values = new Values(this.targetMap.values());
255                        this.values = values;
256                }
257                return values;
258        }
259
260        @Override
261        public Set<Entry<String, V>> entrySet() {
262                Set<Entry<String, V>> entrySet = this.entrySet;
263                if (entrySet == null) {
264                        entrySet = new EntrySet(this.targetMap.entrySet());
265                        this.entrySet = entrySet;
266                }
267                return entrySet;
268        }
269
270        @Override
271        public LinkedCaseInsensitiveMap<V> clone() {
272                return new LinkedCaseInsensitiveMap<>(this);
273        }
274
275        @Override
276        public boolean equals(@Nullable Object other) {
277                return (this == other || this.targetMap.equals(other));
278        }
279
280        @Override
281        public int hashCode() {
282                return this.targetMap.hashCode();
283        }
284
285        @Override
286        public String toString() {
287                return this.targetMap.toString();
288        }
289
290
291        // Specific to LinkedCaseInsensitiveMap
292
293        /**
294         * Return the locale used by this {@code LinkedCaseInsensitiveMap}.
295         * Used for case-insensitive key conversion.
296         * @since 4.3.10
297         * @see #LinkedCaseInsensitiveMap(Locale)
298         * @see #convertKey(String)
299         */
300        public Locale getLocale() {
301                return this.locale;
302        }
303
304        /**
305         * Convert the given key to a case-insensitive key.
306         * <p>The default implementation converts the key
307         * to lower-case according to this Map's Locale.
308         * @param key the user-specified key
309         * @return the key to use for storing
310         * @see String#toLowerCase(Locale)
311         */
312        protected String convertKey(String key) {
313                return key.toLowerCase(getLocale());
314        }
315
316        /**
317         * Determine whether this map should remove the given eldest entry.
318         * @param eldest the candidate entry
319         * @return {@code true} for removing it, {@code false} for keeping it
320         * @see LinkedHashMap#removeEldestEntry
321         */
322        protected boolean removeEldestEntry(Map.Entry<String, V> eldest) {
323                return false;
324        }
325
326        @Nullable
327        private String removeCaseInsensitiveKey(String key) {
328                return this.caseInsensitiveKeys.remove(convertKey(key));
329        }
330
331
332        private class KeySet extends AbstractSet<String> {
333
334                private final Set<String> delegate;
335
336                KeySet(Set<String> delegate) {
337                        this.delegate = delegate;
338                }
339
340                @Override
341                public int size() {
342                        return this.delegate.size();
343                }
344
345                @Override
346                public boolean contains(Object o) {
347                        return this.delegate.contains(o);
348                }
349
350                @Override
351                public Iterator<String> iterator() {
352                        return new KeySetIterator();
353                }
354
355                @Override
356                public boolean remove(Object o) {
357                        return LinkedCaseInsensitiveMap.this.remove(o) != null;
358                }
359
360                @Override
361                public void clear() {
362                        LinkedCaseInsensitiveMap.this.clear();
363                }
364
365                @Override
366                public Spliterator<String> spliterator() {
367                        return this.delegate.spliterator();
368                }
369
370                @Override
371                public void forEach(Consumer<? super String> action) {
372                        this.delegate.forEach(action);
373                }
374        }
375
376
377        private class Values extends AbstractCollection<V> {
378
379                private final Collection<V> delegate;
380
381                Values(Collection<V> delegate) {
382                        this.delegate = delegate;
383                }
384
385                @Override
386                public int size() {
387                        return this.delegate.size();
388                }
389
390                @Override
391                public boolean contains(Object o) {
392                        return this.delegate.contains(o);
393                }
394
395                @Override
396                public Iterator<V> iterator() {
397                        return new ValuesIterator();
398                }
399
400                @Override
401                public void clear() {
402                        LinkedCaseInsensitiveMap.this.clear();
403                }
404
405                @Override
406                public Spliterator<V> spliterator() {
407                        return this.delegate.spliterator();
408                }
409
410                @Override
411                public void forEach(Consumer<? super V> action) {
412                        this.delegate.forEach(action);
413                }
414        }
415
416
417        private class EntrySet extends AbstractSet<Entry<String, V>> {
418
419                private final Set<Entry<String, V>> delegate;
420
421                public EntrySet(Set<Entry<String, V>> delegate) {
422                        this.delegate = delegate;
423                }
424
425                @Override
426                public int size() {
427                        return this.delegate.size();
428                }
429
430                @Override
431                public boolean contains(Object o) {
432                        return this.delegate.contains(o);
433                }
434
435                @Override
436                public Iterator<Entry<String, V>> iterator() {
437                        return new EntrySetIterator();
438                }
439
440                @Override
441                @SuppressWarnings("unchecked")
442                public boolean remove(Object o) {
443                        if (this.delegate.remove(o)) {
444                                removeCaseInsensitiveKey(((Map.Entry<String, V>) o).getKey());
445                                return true;
446                        }
447                        return false;
448                }
449
450                @Override
451                public void clear() {
452                        this.delegate.clear();
453                        caseInsensitiveKeys.clear();
454                }
455
456                @Override
457                public Spliterator<Entry<String, V>> spliterator() {
458                        return this.delegate.spliterator();
459                }
460
461                @Override
462                public void forEach(Consumer<? super Entry<String, V>> action) {
463                        this.delegate.forEach(action);
464                }
465        }
466
467
468        private abstract class EntryIterator<T> implements Iterator<T> {
469
470                private final Iterator<Entry<String, V>> delegate;
471
472                @Nullable
473                private Entry<String, V> last;
474
475                public EntryIterator() {
476                        this.delegate = targetMap.entrySet().iterator();
477                }
478
479                protected Entry<String, V> nextEntry() {
480                        Entry<String, V> entry = this.delegate.next();
481                        this.last = entry;
482                        return entry;
483                }
484
485                @Override
486                public boolean hasNext() {
487                        return this.delegate.hasNext();
488                }
489
490                @Override
491                public void remove() {
492                        this.delegate.remove();
493                        if (this.last != null) {
494                                removeCaseInsensitiveKey(this.last.getKey());
495                                this.last = null;
496                        }
497                }
498        }
499
500
501        private class KeySetIterator extends EntryIterator<String> {
502
503                @Override
504                public String next() {
505                        return nextEntry().getKey();
506                }
507        }
508
509
510        private class ValuesIterator extends EntryIterator<V> {
511
512                @Override
513                public V next() {
514                        return nextEntry().getValue();
515                }
516        }
517
518
519        private class EntrySetIterator extends EntryIterator<Entry<String, V>> {
520
521                @Override
522                public Entry<String, V> next() {
523                        return nextEntry();
524                }
525        }
526
527}