1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package libcore.java.util; 18 19import static java.util.Locale.LanguageRange.MAX_WEIGHT; 20 21import java.util.ArrayList; 22import java.util.Arrays; 23import java.util.Collections; 24import java.util.HashMap; 25import java.util.List; 26import java.util.Locale.LanguageRange; 27import java.util.Map; 28import java.util.stream.Collectors; 29 30import junit.framework.TestCase; 31 32/** 33 * Tests {@link LanguageRange}. 34 */ 35public class LocaleLanguageRangeTest extends TestCase { 36 37 /** 38 * Checks that the constants for min/max weight don't accidentally change. 39 */ 40 public void testWeight_constantValues() { 41 assertEquals(0.0, LanguageRange.MIN_WEIGHT); 42 assertEquals(1.0, MAX_WEIGHT); 43 } 44 45 public void testConstructor_defaultsToMaxWeight() { 46 assertEquals(MAX_WEIGHT, new LanguageRange("de-DE").getWeight()); 47 } 48 49 public void testConstructor_invalidWeight() { 50 try { 51 new LanguageRange("de-DE", -0.00000001); 52 fail(); 53 } catch (IllegalArgumentException expected) { 54 55 } 56 try { 57 new LanguageRange("de-DE", 1.00000001); 58 fail(); 59 } catch (IllegalArgumentException expected) { 60 } 61 // These work: 62 new LanguageRange("de-DE", 0); 63 new LanguageRange("de-DE", 1); 64 } 65 66 public void testConstructor_nullRange() { 67 try { 68 new LanguageRange(null); 69 fail(); 70 } catch (NullPointerException expected) { 71 } 72 try { 73 new LanguageRange(null, MAX_WEIGHT); 74 fail(); 75 } catch (NullPointerException expected) { 76 } 77 new LanguageRange("de-DE", MAX_WEIGHT); // works 78 } 79 80 public void testConstructor_checksForAtLeastOneSubtag() { 81 assertRangeMalformed(""); 82 // The fact that ArrayIndexOutOfBoundsException instead of 83 // IllegalArgumentException is thrown here is somewhat 84 // inconsistent; the checks below ensure that we're aware 85 // if we change the behavior in future. 86 try { 87 new LanguageRange("-"); 88 fail(); 89 } catch (ArrayIndexOutOfBoundsException expected) { 90 } 91 try { 92 new LanguageRange("--"); 93 fail(); 94 } catch (ArrayIndexOutOfBoundsException expected) { 95 } 96 } 97 98 public void testConstructor_checksForWellFormedSubtags() { 99 // first subtag must not have digits 100 assertRangeMalformed("012-xx"); 101 assertRangeMalformed("b0b-xx"); 102 new LanguageRange("bob-xx"); // okay 103 new LanguageRange("bob-01"); // okay 104 105 // subtags must be <= 8 characters 106 assertRangeMalformed("de-abcdefghi-xx"); 107 new LanguageRange("de-abcdefgh-xx"); // okay 108 109 // "-" only between subtags and only one in a row 110 assertRangeMalformed("-de"); 111 assertRangeMalformed("de-"); 112 assertRangeMalformed("de--DE"); 113 new LanguageRange("de-DE"); // okay 114 new LanguageRange("de"); // okay 115 } 116 117 public void testConstructor_acceptsWildcardSubtags() { 118 new LanguageRange("de-*"); 119 new LanguageRange("*-DE"); 120 new LanguageRange("de-*-DE"); 121 new LanguageRange("*"); 122 } 123 124 public void testEqualsAndHashCode() { 125 checkEqual(new LanguageRange("en-US"), new LanguageRange("en-US")); 126 checkNotEqual(new LanguageRange("en-US"), new LanguageRange("en-AU")); 127 128 checkEqual(new LanguageRange("en-US"), 129 new LanguageRange("en-US", LanguageRange.MAX_WEIGHT)); 130 checkNotEqual(new LanguageRange("en-US"), new LanguageRange("en-US", 0.4)); 131 132 checkEqual(new LanguageRange("en-US", 0.3), new LanguageRange("en-US", 0.3)); 133 checkNotEqual(new LanguageRange("en-US", 0.3), new LanguageRange("en-US", 0.4)); 134 checkNotEqual(new LanguageRange("ja-JP", 0.5), new LanguageRange("de-DE", 0.5)); 135 } 136 137 private static <T> void checkEqual(T a, T b) { 138 assertEquals(a, b); 139 assertEquals(b, a); 140 assertEquals(a.hashCode(), b.hashCode()); 141 } 142 143 private static <T> void checkNotEqual(T a, T b) { 144 assertFalse(a.equals(b)); 145 assertFalse(b.equals(a)); 146 assertTrue(a.hashCode() != b.hashCode()); 147 } 148 149 public void testGetRange() { 150 assertEquals("de-de", new LanguageRange("de-DE", 0.12345).getRange()); 151 } 152 153 public void testGetWeight() { 154 assertEquals(0.12345, new LanguageRange("de-DE", 0.12345).getWeight()); 155 } 156 157 public void testMapEquivalents_emptyList() { 158 List<LanguageRange> noRange = Collections.emptyList(); 159 assertEquals(noRange, LanguageRange.mapEquivalents(noRange, Collections.emptyMap())); 160 assertEquals(noRange, LanguageRange.mapEquivalents(noRange, 161 Collections.singletonMap("en-US", Arrays.asList("en-US", "en-AU", "en-UK")))); 162 } 163 164 public void testMapEquivalents_emptyMap_createsModifiableCopy() { 165 List<LanguageRange> inputRanges = Collections.unmodifiableList(Arrays.asList( 166 new LanguageRange("de-DE"), 167 new LanguageRange("ja-JP"))); 168 List<LanguageRange> outputRanges = 169 LanguageRange.mapEquivalents(inputRanges, Collections.emptyMap()); 170 assertEquals(inputRanges, outputRanges); 171 assertNotSame(inputRanges, outputRanges); 172 // result is modifiable 173 outputRanges.add(new LanguageRange("fr-FR")); 174 outputRanges.clear(); 175 } 176 177 /** 178 * Tests the example from the {@link LanguageRange#mapEquivalents(List, Map)} documentation. 179 */ 180 public void testMapEquivalents_exampleFromDocumentation() { 181 Map<String, List<String>> map = new HashMap<>(); 182 map.put("zh", Collections.unmodifiableList(Arrays.asList("zh", "zh-Hans"))); 183 map.put("zh-HK", Collections.singletonList("zh-HK")); 184 map.put("zh-TW", Collections.singletonList("zh-TW")); 185 186 List<LanguageRange> inputPriorityList = Arrays.asList( 187 new LanguageRange("zh"), 188 new LanguageRange("zh-CN"), 189 new LanguageRange("en"), 190 new LanguageRange("zh-TW"), 191 new LanguageRange("zh-TW") 192 ); 193 List<LanguageRange> expectedOutput = Arrays.asList( 194 new LanguageRange("zh"), 195 new LanguageRange("zh-Hans"), 196 new LanguageRange("zh-CN"), 197 new LanguageRange("zh-Hans-CN"), 198 new LanguageRange("en"), 199 new LanguageRange("zh-TW"), 200 new LanguageRange("zh-TW") 201 ); 202 List<LanguageRange> outputProrityList = LanguageRange 203 .mapEquivalents(inputPriorityList, map); 204 assertEquals(expectedOutput, outputProrityList); 205 } 206 207 public void testMapEquivalents_nullList() { 208 try { 209 LanguageRange.mapEquivalents(null, Collections.emptyMap()); 210 fail(); 211 } catch (NullPointerException expected) { 212 } 213 } 214 215 /** 216 * The documentation doesn't specify whether {@code mapEquivalents()} accepts a 217 * null map, but the current behavior is the same as for an empty map. This test 218 * ensures that we're aware if this behavior changse. 219 */ 220 public void testMapEquivalents_nullMap() { 221 List<LanguageRange> priorityList = Collections.unmodifiableList(Arrays.asList( 222 new LanguageRange("de-DE"), 223 new LanguageRange("en-UK"), 224 new LanguageRange("zh-CN"))); 225 assertEquals(priorityList, LanguageRange.mapEquivalents(priorityList, null)); 226 } 227 228 /** Tests {@link LanguageRange#parse(String, Map)}. */ 229 public void testMapEquivalents() { 230 List<LanguageRange> expected = Arrays.asList( 231 new LanguageRange("de-de", 1.0), 232 new LanguageRange("en-us", 0.7), 233 new LanguageRange("en-au", 0.7) 234 ); 235 Map<String, List<String>> map = new HashMap<>(); 236 map.put("fr", Arrays.asList("de-DE")); 237 map.put("en", Arrays.asList("en-US", "en-AU")); 238 String ranges = "Accept-Language: fr,en;q=0.7"; 239 assertEquals(expected, LanguageRange.parse(ranges, map)); 240 // Per the documentation, this should be equivalent 241 assertEquals(expected, LanguageRange.mapEquivalents(LanguageRange.parse(ranges), map)); 242 } 243 244 /** 245 * Because {@code mapEquivalents(ranges, map)} behaves identically 246 * to {@code mapEquivalents(parse(ranges), map}, any equivalent 247 * locales from {@link sun.util.locale.LocaleEquivalentMaps}, 248 * such as {@code "iw" -> "he"}, are expanded before the mapping 249 * from {@code map} is applied. 250 */ 251 public void testParse_map_localeEquivalent() { 252 Map<String, List<String>> map = new HashMap<>(); 253 map.put("iw", Arrays.asList("de-DE")); 254 map.put("en", Arrays.asList("en-US", "en-AU")); 255 256 List<LanguageRange> expectedOutput = Arrays.asList( 257 new LanguageRange("de-de", 1.0), // iw -> de-de (map) 258 new LanguageRange("he", 1.0), // iw -> he (LocaleEquivalentMaps) 259 new LanguageRange("en-us", 0.7), // en -> en-us (map) 260 new LanguageRange("en-au", 0.7)); // en -> en-au (map) 261 262 String ranges = "Accept-Language: iw,en;q=0.7"; 263 assertEquals(expectedOutput, LanguageRange.parse(ranges, map)); 264 // Per the documentation, this should be equivalent 265 assertEquals(expectedOutput, 266 LanguageRange.mapEquivalents(LanguageRange.parse(ranges), map)); 267 } 268 269 /** 270 * Tests the example from the {@link LanguageRange#parse(String)} documentation. 271 */ 272 public void testParse_acceptLanguage_exampleFromDocumentation() { 273 List<LanguageRange> expected = Arrays.asList( 274 new LanguageRange("iw", 1.0), 275 new LanguageRange("he", 1.0), 276 new LanguageRange("en-us", 0.7), 277 new LanguageRange("en", 0.3) 278 ); 279 assertEquals(expected, LanguageRange.parse("Accept-Language: iw,en-us;q=0.7,en;q=0.3")); 280 } 281 282 /** 283 * Tests parsing the example from RFC 2616 section 14.4. 284 */ 285 public void testParse_acceptLanguage_exampleFromRfc2616() { 286 List<LanguageRange> expected = Arrays.asList( 287 new LanguageRange("da", 1.0), 288 new LanguageRange("en-gb", 0.8), 289 new LanguageRange("en", 0.7) 290 ); 291 assertEquals(expected, LanguageRange.parse("Accept-Language: da, en-gb;q=0.8, en;q=0.7")); 292 } 293 294 public void testParse_acceptLanguage_malformed() { 295 try { 296 LanguageRange.parse("Accept-Language: fr,en-us;q=1;q=0.5"); 297 fail(); 298 } catch (IllegalArgumentException expected) { 299 } 300 try { 301 LanguageRange.parse("Accept-Language: q=0.5"); 302 fail(); 303 } catch (IllegalArgumentException expected) { 304 } 305 try { 306 LanguageRange.parse("Accept-Language: ;q=0.5"); 307 fail(); 308 } catch (IllegalArgumentException expected) { 309 } 310 try { 311 LanguageRange.parse("Accept-Language: thislanguagetagistoolong;q=0.5"); 312 fail(); 313 } catch (IllegalArgumentException expected) { 314 } 315 } 316 317 /** 318 * The current implementation doesn't require a ' ' after the "Accept-Language:". 319 * This test ensures that we're aware if this behavior changes. 320 */ 321 public void testParse_acceptLanguage_missingSpaceAfterColon() { 322 List<LanguageRange> languageRanges = Arrays.asList( 323 new LanguageRange("fr"), 324 new LanguageRange("en-us", 1) 325 ); 326 assertEquals(languageRanges, LanguageRange.parse("Accept-Language:fr,en-us;q=1")); 327 } 328 329 public void testParse_acceptLanguage_wildCards() { 330 List<LanguageRange> expected = Arrays.asList( 331 new LanguageRange("da", 1.0), 332 new LanguageRange("en-*", 0.8), 333 new LanguageRange("*", 0.7) 334 ); 335 assertEquals(expected, LanguageRange.parse("Accept-Language: da, en-*;q=0.8, *;q=0.7")); 336 } 337 338 public void testParse_acceptLanguage_weightValid() { 339 LanguageRange fr = new LanguageRange("fr"); 340 assertEquals(Arrays.asList(fr, new LanguageRange("en-us", 1.0)), 341 LanguageRange.parse("Accept-Language: fr,en-us;q=1")); 342 assertEquals(Arrays.asList(fr, new LanguageRange("en-us", 0.1)), 343 LanguageRange.parse("Accept-Language: fr,en-us;q=.1")); 344 assertEquals(Arrays.asList(fr, new LanguageRange("en-us", 0.12345678901234567890)), 345 LanguageRange.parse("Accept-Language: fr,en-us;q=0.12345678901234567890")); 346 assertEquals(Arrays.asList(fr, new LanguageRange("en-us", 0)), 347 LanguageRange.parse("Accept-Language: fr,en-us;q=0")); 348 } 349 350 public void testParse_acceptLanguage_weightInvalid() { 351 try { 352 LanguageRange.parse("Accept-Language: iw,en-us;q=1.1"); 353 fail(); 354 } catch (IllegalArgumentException expected) { 355 } 356 try { 357 LanguageRange.parse("Accept-Language: iw,en-us;q=-0.1"); 358 fail(); 359 } catch (IllegalArgumentException expected) { 360 } 361 } 362 363 // Based on a test case that was contributed back to upstream maintainers through 364 // https://bugs.openjdk.java.net/browse/JDK-8166994 365 public void testParse_multiEquivalent_consistency() { 366 List<String> parsed = rangesToStrings(LanguageRange.parse("ccq-xx")); 367 368 assertEquals(parsed, rangesToStrings(LanguageRange.parse("ccq-xx"))); // consistency 369 assertEquals(Arrays.asList("ccq-xx", "ybd-xx", "rki-xx"), parsed); // expected result 370 } 371 372 /** 373 * Tests parsing a Locale range matching an entry from 374 * {@link sun.util.locale.LocaleEquivalentMaps#singleEquivMap}. 375 */ 376 public void testParse_singleEquivalent() { 377 assertParseRanges("art-lojban", "jbo"); // example from RFC 4647 section 3.2 378 assertParseRanges("yue", "zh-yue"); 379 assertParseRanges("yue-xx", "zh-yue-xx"); 380 } 381 382 /** 383 * Tests parsing a Locale range matching an entry from 384 * {@link sun.util.locale.LocaleEquivalentMaps#multiEquivsMap}. 385 */ 386 public void testParse_multiEquivalent() { 387 assertParseRanges("mst", "myt", "mry"); 388 assertParseRanges("i-hak", "zh-hakka", "hak"); 389 } 390 391 /** 392 * Tests parsing a Locale range matching an entry from 393 * {@link sun.util.locale.LocaleEquivalentMaps#regionVariantEquivMap}. 394 */ 395 public void testParse_regionEquivalent() { 396 // Region ("-de" or "-dd") matches the end 397 assertParseRanges("de-de", "de-dd"); 398 assertParseRanges("xx-dd", "xx-de"); 399 400 // Region ("-de" or "-dd") matches the middle 401 assertParseRanges("xx-de-yy", "xx-dd-yy"); 402 assertParseRanges("xx-dd-yy", "xx-de-yy"); 403 404 assertParseRanges("xx-bu", "xx-mm"); 405 assertParseRanges("xx-mm", "xx-bu"); 406 } 407 408 /** 409 * Tests parsing a Locale range matching entries from both 410 * {@link sun.util.locale.LocaleEquivalentMaps#singleEquivMap} and 411 * {@link sun.util.locale.LocaleEquivalentMaps#regionVariantEquivMap}. 412 */ 413 public void testParse_singleAndRegionEquivalent() { 414 assertParseRanges("sgn-ch-de", "sgg", "sgn-ch-dd"); 415 assertParseRanges("sgn-ch-de-xx", "sgg-xx", "sgn-ch-dd-xx"); 416 } 417 418 /** 419 * Asserts that {@code LanguageRange(ranges)} returns LanguageRanges whose 420 * {@link LanguageRange#getRange() Range string}s are {@code ranges} and 421 * {@code expectedAdditional}, in order. 422 */ 423 private static void assertParseRanges(String ranges, String... expectedAdditional) { 424 List<String> expected = new ArrayList<>(); 425 expected.add(ranges); 426 expected.addAll(Arrays.asList(expectedAdditional)); 427 428 List<String> actual = rangesToStrings(LanguageRange.parse(ranges)); 429 430 assertEquals(expected, actual); 431 } 432 433 private static List<String> rangesToStrings(List<LanguageRange> languageRanges) { 434 return languageRanges.stream().map(LanguageRange::getRange).collect(Collectors.toList()); 435 } 436 437 private void assertRangeMalformed(String range) { 438 try { 439 new LanguageRange(range); 440 fail("Range should be recognized as malformed: " + range); 441 } catch (IllegalArgumentException expected) { 442 // Check for the exception that is thrown when a malformed subtag is detected. 443 // The exception message used here may change in future. 444 assertEquals("range=" + range.toLowerCase(), expected.getMessage()); 445 } 446 } 447 448} 449