diff --git a/src/main/java/org/apache/commons/collections4/map/CaseInsensitiveMap.java b/src/main/java/org/apache/commons/collections4/map/CaseInsensitiveMap.java index e1e7daf011..a6238be41d 100644 --- a/src/main/java/org/apache/commons/collections4/map/CaseInsensitiveMap.java +++ b/src/main/java/org/apache/commons/collections4/map/CaseInsensitiveMap.java @@ -141,6 +141,63 @@ protected Object convertKey(final Object key) { return AbstractHashedMap.NULL; } + @Override + public V put(final K key, final V value) { + final Object convertedKey = convertKey(key); + final int hashCode = hash(convertedKey); + final int index = hashIndex(hashCode, data.length); + HashEntry entry = data[index]; + while (entry != null) { + if (entry.hashCode == hashCode && isEqualKey(convertedKey, entry.key)) { + final V oldValue = entry.getValue(); + updateEntry(entry, value); + return oldValue; + } + entry = entry.next; + } + + addMappingWithoutKeyConversion(index, hashCode, convertedKey, value); + return null; + } + + /** + * Adds a new key-value mapping into this map. + *

+ * This implementation calls {@code createEntryWithoutKeyConversion()}, {@code addEntry()} + * and {@code checkCapacity()}. + * It also handles changes to {@code modCount} and {@code size}. + * Subclasses could override to fully control adds to the map. + * + * @param hashIndex the index into the data array to store at + * @param hashCode the hash code of the key to add + * @param convertedKey the key to add, already converted to lower case by {@code convertKey()} + * @param value the value to add + */ + protected void addMappingWithoutKeyConversion(final int hashIndex, final int hashCode, final Object convertedKey, final V value) { + modCount++; + final HashEntry entry = createEntryWithoutKeyConversion(data[hashIndex], hashCode, convertedKey, value); + addEntry(entry, hashIndex); + size++; + checkCapacity(); + } + + /** + * Creates an entry to store the key-value data. + *

+ * This implementation creates a new HashEntry instance. + * Subclasses can override this to return a different storage class, + * or implement caching. + * + * @param next the next entry in sequence + * @param hashCode the hash code to use + * @param convertedKey the key to add, already converted to lower case by {@code convertKey()} + * @param value the value to store + * @return the newly created entry + */ + protected HashEntry createEntryWithoutKeyConversion(final HashEntry next, final int hashCode, final Object convertedKey, final V value) { + return new HashEntry<>(next, hashCode, convertedKey, value); + } + /** * Clones the map without cloning the keys or values. * diff --git a/src/test/java/org/apache/commons/collections4/map/CaseInsensitiveMapTest.java b/src/test/java/org/apache/commons/collections4/map/CaseInsensitiveMapTest.java index 13d503b3e2..a635b00ba8 100644 --- a/src/test/java/org/apache/commons/collections4/map/CaseInsensitiveMapTest.java +++ b/src/test/java/org/apache/commons/collections4/map/CaseInsensitiveMapTest.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Set; +import org.easymock.EasyMock; import org.junit.jupiter.api.Test; /** @@ -147,4 +148,20 @@ public void testPutAll() { || !caseInsensitiveMap.containsValue("Three")); // ones collapsed assertEquals("Four", caseInsensitiveMap.get(null)); } + + // COLLECTIONS-803 + @Test + @SuppressWarnings("unchecked") + public void testPutConvertKeyOnlyOnce() { + CaseInsensitiveMap mock = EasyMock.partialMockBuilder(CaseInsensitiveMap.class) + .addMockedMethod("convertKey") + .createMock(); + mock.data = new AbstractHashedMap.HashEntry[16]; + EasyMock.expect(mock.convertKey("Key")).andReturn("key").once(); + EasyMock.replay(mock); + + mock.put("Key", "value"); + + EasyMock.verify(mock); + } }