Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
/**
* A case-insensitive {@code Map}.
* <p>
* Before keys are added to the map or compared to other existing keys, they are converted
* to all lowercase in a locale-independent fashion by using information from the Unicode
* data file.
* Before keys are added to the map or compared to other existing keys, each of their
* characters (Unicode code points) is converted with
* {@code Character.toLowerCase(Character.toUpperCase(codePoint))}, that is with the simple,
* one-to-one case mappings of the Unicode data file, independently of the locale. This is not
* the same as {@link String#toLowerCase(java.util.Locale)}: for example, U+03D0 (GREEK BETA
* SYMBOL) and U+03B2 (GREEK SMALL LETTER BETA) give the same key.
* </p>
* <p>
* Null keys are supported.
Expand Down Expand Up @@ -131,8 +134,8 @@ public CaseInsensitiveMap<K, V> clone() {
}

/**
* Overrides convertKey() from {@link AbstractHashedMap} to convert keys to
* lower case.
* Overrides convertKey() from {@link AbstractHashedMap} to convert each code point of
* keys with {@code Character.toLowerCase(Character.toUpperCase(codePoint))}.
* <p>
* Returns {@link AbstractHashedMap#NULL} if key is null.
*
Expand All @@ -142,11 +145,14 @@ public CaseInsensitiveMap<K, V> clone() {
@Override
protected Object convertKey(final Object key) {
if (key != null) {
final char[] chars = key.toString().toCharArray();
for (int i = chars.length - 1; i >= 0; i--) {
chars[i] = Character.toLowerCase(Character.toUpperCase(chars[i]));
final String keyString = key.toString();
final StringBuilder converted = new StringBuilder(keyString.length());
for (int i = 0; i < keyString.length();) {
final int codePoint = keyString.codePointAt(i);
converted.appendCodePoint(Character.toLowerCase(Character.toUpperCase(codePoint)));
i += Character.charCount(codePoint);
}
return new String(chars);
return converted.toString();
}
return NULL;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.InvalidObjectException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -93,6 +95,57 @@ void testInitialCapacityZero() {
assertEquals(1, map.data.length);
}

/**
* Test for <a href="https://issues.apache.org/jira/browse/COLLECTIONS-899">COLLECTIONS-899</a>:
* characters outside the Basic Multilingual Plane are converted like the other characters.
*/
@Test
void testSupplementaryCharacters() throws Exception {
final String capitalLongI = new String(Character.toChars(0x10400)); // DESERET CAPITAL LETTER LONG I
final String smallLongI = new String(Character.toChars(0x10428)); // DESERET SMALL LETTER LONG I
final String capitalLongE = new String(Character.toChars(0x10401)); // DESERET CAPITAL LETTER LONG E
final CaseInsensitiveMap<String, String> map = new CaseInsensitiveMap<>();
map.put(capitalLongI, "value");
map.put("a" + capitalLongI + "B", "mixed");
assertEquals("value", map.get(smallLongI));
assertEquals("mixed", map.get("A" + smallLongI + "b"));
assertEquals(new HashSet<>(Arrays.asList(smallLongI, "a" + smallLongI + "b")), map.keySet());

map.put(smallLongI, "other value");
assertEquals(2, map.size());
assertEquals("other value", map.get(capitalLongI));
assertEquals("other value", serializeDeserialize(map).get(capitalLongI));

map.put(capitalLongE, "other letter");
assertEquals(3, map.size());
}

/**
* Characters without case, and unpaired surrogates, are kept as they are.
*/
@Test
void testUncasedSupplementaryCharactersAndUnpairedSurrogates() {
final String grinningFace = new String(Character.toChars(0x1F600));
final CaseInsensitiveMap<String, String> map = new CaseInsensitiveMap<>();
map.put(grinningFace, "emoji");
map.put("\uD801", "high surrogate");
map.put("x\uDC28", "low surrogate");
assertEquals(new HashSet<>(Arrays.asList(grinningFace, "\uD801", "x\uDC28")), map.keySet());
}

/**
* Keys are converted again when a map is deserialized, so a converted key must convert to itself.
*/
@Test
void testConversionIsIdempotent() {
final CaseInsensitiveMap<String, String> map = new CaseInsensitiveMap<>();
for (int codePoint = 0; codePoint <= Character.MAX_CODE_POINT; codePoint++) {
final int current = codePoint;
final Object converted = map.convertKey(new String(Character.toChars(current)));
assertEquals(converted, map.convertKey(converted), () -> "U+" + Integer.toHexString(current));
}
}

// COLLECTIONS-294
@Test
void testLocaleIndependence() {
Expand Down