Skip to content

Commit 55ac110

Browse files
committed
Fix LinkedCaseInsensitiveMap collection methods
Ensure that results returned from keySet, entrySet & values are tracked to remove case insensitive keys from the source map. Closes gh-22821
1 parent c4bd5ab commit 55ac110

File tree

3 files changed

+338
-11
lines changed

3 files changed

+338
-11
lines changed

spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java

Lines changed: 245 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,12 +17,17 @@
1717
package org.springframework.util;
1818

1919
import java.io.Serializable;
20+
import java.util.AbstractCollection;
21+
import java.util.AbstractSet;
2022
import java.util.Collection;
2123
import java.util.HashMap;
24+
import java.util.Iterator;
2225
import java.util.LinkedHashMap;
2326
import java.util.Locale;
2427
import java.util.Map;
2528
import java.util.Set;
29+
import java.util.Spliterator;
30+
import java.util.function.Consumer;
2631
import java.util.function.Function;
2732

2833
import org.springframework.lang.Nullable;
@@ -37,6 +42,7 @@
3742
* <p>Does <i>not</i> support {@code null} keys.
3843
*
3944
* @author Juergen Hoeller
45+
* @author Phillip Webb
4046
* @since 3.0
4147
* @param <V> the value type
4248
*/
@@ -49,6 +55,12 @@ public class LinkedCaseInsensitiveMap<V> implements Map<String, V>, Serializable
4955

5056
private final Locale locale;
5157

58+
private transient Set<String> keySet;
59+
60+
private transient Collection<V> values;
61+
62+
private transient Set<Entry<String, V>> entrySet;
63+
5264

5365
/**
5466
* Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys
@@ -98,7 +110,7 @@ public boolean containsKey(Object key) {
98110
protected boolean removeEldestEntry(Map.Entry<String, V> eldest) {
99111
boolean doRemove = LinkedCaseInsensitiveMap.this.removeEldestEntry(eldest);
100112
if (doRemove) {
101-
caseInsensitiveKeys.remove(convertKey(eldest.getKey()));
113+
removeCaseInsensitiveKey(eldest.getKey());
102114
}
103115
return doRemove;
104116
}
@@ -208,7 +220,7 @@ public V computeIfAbsent(String key, Function<? super String, ? extends V> mappi
208220
@Nullable
209221
public V remove(Object key) {
210222
if (key instanceof String) {
211-
String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key));
223+
String caseInsensitiveKey = removeCaseInsensitiveKey((String) key);
212224
if (caseInsensitiveKey != null) {
213225
return this.targetMap.remove(caseInsensitiveKey);
214226
}
@@ -224,17 +236,32 @@ public void clear() {
224236

225237
@Override
226238
public Set<String> keySet() {
227-
return this.targetMap.keySet();
239+
Set<String> keySet = this.keySet;
240+
if (keySet == null) {
241+
keySet = new KeySet(this.targetMap.keySet());
242+
this.keySet = keySet;
243+
}
244+
return keySet;
228245
}
229246

230247
@Override
231248
public Collection<V> values() {
232-
return this.targetMap.values();
249+
Collection<V> values = this.values;
250+
if (values == null) {
251+
values = new Values(this.targetMap.values());
252+
this.values = values;
253+
}
254+
return values;
233255
}
234256

235257
@Override
236258
public Set<Entry<String, V>> entrySet() {
237-
return this.targetMap.entrySet();
259+
Set<Entry<String, V>> entrySet = this.entrySet;
260+
if (entrySet == null) {
261+
entrySet = new EntrySet(this.targetMap.entrySet());
262+
this.entrySet = entrySet;
263+
}
264+
return entrySet;
238265
}
239266

240267
@Override
@@ -293,4 +320,216 @@ protected boolean removeEldestEntry(Map.Entry<String, V> eldest) {
293320
return false;
294321
}
295322

323+
private String removeCaseInsensitiveKey(String key) {
324+
return this.caseInsensitiveKeys.remove(convertKey(key));
325+
}
326+
327+
328+
private class KeySet extends AbstractSet<String> {
329+
330+
private final Set<String> delegate;
331+
332+
333+
KeySet(Set<String> delegate) {
334+
this.delegate = delegate;
335+
}
336+
337+
338+
@Override
339+
public int size() {
340+
return this.delegate.size();
341+
}
342+
343+
@Override
344+
public boolean contains(Object o) {
345+
return this.delegate.contains(o);
346+
}
347+
348+
@Override
349+
public Iterator<String> iterator() {
350+
return new KeySetIterator();
351+
}
352+
353+
@Override
354+
public boolean remove(Object o) {
355+
return LinkedCaseInsensitiveMap.this.remove(o) != null;
356+
}
357+
358+
@Override
359+
public void clear() {
360+
LinkedCaseInsensitiveMap.this.clear();
361+
}
362+
363+
@Override
364+
public Spliterator<String> spliterator() {
365+
return this.delegate.spliterator();
366+
}
367+
368+
@Override
369+
public void forEach(Consumer<? super String> action) {
370+
this.delegate.forEach(action);
371+
}
372+
373+
}
374+
375+
376+
private class Values extends AbstractCollection<V> {
377+
378+
private final Collection<V> delegate;
379+
380+
381+
Values(Collection<V> delegate) {
382+
this.delegate = delegate;
383+
}
384+
385+
386+
@Override
387+
public int size() {
388+
return this.delegate.size();
389+
}
390+
391+
@Override
392+
public boolean contains(Object o) {
393+
return this.delegate.contains(o);
394+
}
395+
396+
@Override
397+
public Iterator<V> iterator() {
398+
return new ValuesIterator();
399+
}
400+
401+
@Override
402+
public void clear() {
403+
LinkedCaseInsensitiveMap.this.clear();
404+
}
405+
406+
@Override
407+
public Spliterator<V> spliterator() {
408+
return this.delegate.spliterator();
409+
}
410+
411+
@Override
412+
public void forEach(Consumer<? super V> action) {
413+
this.delegate.forEach(action);
414+
}
415+
416+
}
417+
418+
419+
private class EntrySet extends AbstractSet<Entry<String, V>> {
420+
421+
private final Set<Entry<String, V>> delegate;
422+
423+
424+
public EntrySet(Set<Entry<String, V>> delegate) {
425+
this.delegate = delegate;
426+
}
427+
428+
429+
@Override
430+
public int size() {
431+
return this.delegate.size();
432+
}
433+
434+
@Override
435+
public boolean contains(Object o) {
436+
return this.delegate.contains(o);
437+
}
438+
439+
@Override
440+
public Iterator<Entry<String, V>> iterator() {
441+
return new EntrySetIterator();
442+
}
443+
444+
445+
@Override
446+
@SuppressWarnings("unchecked")
447+
public boolean remove(Object o) {
448+
if (this.delegate.remove(o)) {
449+
removeCaseInsensitiveKey(((Map.Entry<String, V>) o).getKey());
450+
return true;
451+
}
452+
return false;
453+
}
454+
455+
456+
@Override
457+
public void clear() {
458+
this.delegate.clear();
459+
caseInsensitiveKeys.clear();
460+
}
461+
462+
@Override
463+
public Spliterator<Entry<String, V>> spliterator() {
464+
return this.delegate.spliterator();
465+
}
466+
467+
@Override
468+
public void forEach(Consumer<? super Entry<String, V>> action) {
469+
this.delegate.forEach(action);
470+
}
471+
472+
}
473+
474+
475+
private class EntryIterator {
476+
477+
private final Iterator<Entry<String, V>> delegate;
478+
479+
private Entry<String, V> last;
480+
481+
public EntryIterator() {
482+
this.delegate = targetMap.entrySet().iterator();
483+
}
484+
485+
public Entry<String, V> nextEntry() {
486+
Entry<String, V> entry = this.delegate.next();
487+
this.last = entry;
488+
return entry;
489+
}
490+
491+
public boolean hasNext() {
492+
return this.delegate.hasNext();
493+
}
494+
495+
public void remove() {
496+
this.delegate.remove();
497+
if(this.last != null) {
498+
removeCaseInsensitiveKey(this.last.getKey());
499+
this.last = null;
500+
}
501+
}
502+
503+
}
504+
505+
506+
private class KeySetIterator extends EntryIterator implements Iterator<String> {
507+
508+
@Override
509+
public String next() {
510+
return nextEntry().getKey();
511+
}
512+
513+
}
514+
515+
516+
private class ValuesIterator extends EntryIterator implements Iterator<V> {
517+
518+
@Override
519+
public V next() {
520+
return nextEntry().getValue();
521+
}
522+
523+
}
524+
525+
526+
private class EntrySetIterator extends EntryIterator implements Iterator<Entry<String, V>> {
527+
528+
@Override
529+
public Entry<String, V> next() {
530+
return nextEntry();
531+
}
532+
533+
}
534+
296535
}

0 commit comments

Comments
 (0)