1 /* 2 * Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.util.locale; 27 28 import java.util.ArrayList; 29 import java.util.Collection; 30 import java.util.HashMap; 31 import java.util.List; 32 import java.util.Locale; 33 import java.util.Locale.*; 34 import static java.util.Locale.FilteringMode.*; 35 import static java.util.Locale.LanguageRange.*; 36 import java.util.Map; 37 import java.util.Set; 38 import java.util.TreeSet; 39 40 /** 41 * Implementation for BCP47 Locale matching 42 * 43 */ 44 public final class LocaleMatcher { 45 filter(List<LanguageRange> priorityList, Collection<Locale> locales, FilteringMode mode)46 public static List<Locale> filter(List<LanguageRange> priorityList, 47 Collection<Locale> locales, 48 FilteringMode mode) { 49 if (priorityList.isEmpty() || locales.isEmpty()) { 50 return new ArrayList<>(); // need to return a empty mutable List 51 } 52 53 // Create a list of language tags to be matched. 54 List<String> tags = new ArrayList<>(); 55 for (Locale locale : locales) { 56 tags.add(locale.toLanguageTag()); 57 } 58 59 // Filter language tags. 60 List<String> filteredTags = filterTags(priorityList, tags, mode); 61 62 // Create a list of matching locales. 63 List<Locale> filteredLocales = new ArrayList<>(filteredTags.size()); 64 for (String tag : filteredTags) { 65 filteredLocales.add(Locale.forLanguageTag(tag)); 66 } 67 68 return filteredLocales; 69 } 70 filterTags(List<LanguageRange> priorityList, Collection<String> tags, FilteringMode mode)71 public static List<String> filterTags(List<LanguageRange> priorityList, 72 Collection<String> tags, 73 FilteringMode mode) { 74 if (priorityList.isEmpty() || tags.isEmpty()) { 75 return new ArrayList<>(); // need to return a empty mutable List 76 } 77 78 ArrayList<LanguageRange> list; 79 if (mode == EXTENDED_FILTERING) { 80 return filterExtended(priorityList, tags); 81 } else { 82 list = new ArrayList<>(); 83 for (LanguageRange lr : priorityList) { 84 String range = lr.getRange(); 85 if (range.startsWith("*-") 86 || range.indexOf("-*") != -1) { // Extended range 87 if (mode == AUTOSELECT_FILTERING) { 88 return filterExtended(priorityList, tags); 89 } else if (mode == MAP_EXTENDED_RANGES) { 90 if (range.charAt(0) == '*') { 91 range = "*"; 92 } else { 93 range = range.replaceAll("-[*]", ""); 94 } 95 list.add(new LanguageRange(range, lr.getWeight())); 96 } else if (mode == REJECT_EXTENDED_RANGES) { 97 throw new IllegalArgumentException("An extended range \"" 98 + range 99 + "\" found in REJECT_EXTENDED_RANGES mode."); 100 } 101 } else { // Basic range 102 list.add(lr); 103 } 104 } 105 106 return filterBasic(list, tags); 107 } 108 } 109 filterBasic(List<LanguageRange> priorityList, Collection<String> tags)110 private static List<String> filterBasic(List<LanguageRange> priorityList, 111 Collection<String> tags) { 112 int splitIndex = splitRanges(priorityList); 113 List<LanguageRange> nonZeroRanges; 114 List<LanguageRange> zeroRanges; 115 if (splitIndex != -1) { 116 nonZeroRanges = priorityList.subList(0, splitIndex); 117 zeroRanges = priorityList.subList(splitIndex, priorityList.size()); 118 } else { 119 nonZeroRanges = priorityList; 120 zeroRanges = List.of(); 121 } 122 123 List<String> list = new ArrayList<>(); 124 for (LanguageRange lr : nonZeroRanges) { 125 String range = lr.getRange(); 126 if (range.equals("*")) { 127 tags = removeTagsMatchingBasicZeroRange(zeroRanges, tags); 128 return new ArrayList<String>(tags); 129 } else { 130 for (String tag : tags) { 131 // change to lowercase for case-insensitive matching 132 String lowerCaseTag = tag.toLowerCase(Locale.ROOT); 133 if (lowerCaseTag.startsWith(range)) { 134 int len = range.length(); 135 if ((lowerCaseTag.length() == len 136 || lowerCaseTag.charAt(len) == '-') 137 && !caseInsensitiveMatch(list, lowerCaseTag) 138 && !shouldIgnoreFilterBasicMatch(zeroRanges, 139 lowerCaseTag)) { 140 // preserving the case of the input tag 141 list.add(tag); 142 } 143 } 144 } 145 } 146 } 147 148 return list; 149 } 150 151 /** 152 * Removes the tag(s) which are falling in the basic exclusion range(s) i.e 153 * range(s) with q=0 and returns the updated collection. If the basic 154 * language ranges contains '*' as one of its non zero range then instead of 155 * returning all the tags, remove those which are matching the range with 156 * quality weight q=0. 157 */ removeTagsMatchingBasicZeroRange( List<LanguageRange> zeroRange, Collection<String> tags)158 private static Collection<String> removeTagsMatchingBasicZeroRange( 159 List<LanguageRange> zeroRange, Collection<String> tags) { 160 if (zeroRange.isEmpty()) { 161 tags = removeDuplicates(tags); 162 return tags; 163 } 164 165 List<String> matchingTags = new ArrayList<>(); 166 for (String tag : tags) { 167 // change to lowercase for case-insensitive matching 168 String lowerCaseTag = tag.toLowerCase(Locale.ROOT); 169 if (!shouldIgnoreFilterBasicMatch(zeroRange, lowerCaseTag) 170 && !caseInsensitiveMatch(matchingTags, lowerCaseTag)) { 171 matchingTags.add(tag); // preserving the case of the input tag 172 } 173 } 174 175 return matchingTags; 176 } 177 178 /** 179 * Remove duplicate tags from the given {@code tags} by 180 * ignoring case considerations. 181 */ removeDuplicates( Collection<String> tags)182 private static Collection<String> removeDuplicates( 183 Collection<String> tags) { 184 Set<String> distinctTags = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 185 return tags.stream().filter(x -> distinctTags.add(x)) 186 .toList(); 187 } 188 189 /** 190 * Returns true if the given {@code list} contains an element which matches 191 * with the given {@code tag} ignoring case considerations. 192 */ caseInsensitiveMatch(List<String> list, String tag)193 private static boolean caseInsensitiveMatch(List<String> list, String tag) { 194 return list.stream().anyMatch((element) 195 -> (element.equalsIgnoreCase(tag))); 196 } 197 198 /** 199 * The tag which is falling in the basic exclusion range(s) should not 200 * be considered as the matching tag. Ignores the tag matching with the 201 * non-zero ranges, if the tag also matches with one of the basic exclusion 202 * ranges i.e. range(s) having quality weight q=0 203 */ shouldIgnoreFilterBasicMatch( List<LanguageRange> zeroRange, String tag)204 private static boolean shouldIgnoreFilterBasicMatch( 205 List<LanguageRange> zeroRange, String tag) { 206 if (zeroRange.isEmpty()) { 207 return false; 208 } 209 210 for (LanguageRange lr : zeroRange) { 211 String range = lr.getRange(); 212 if (range.equals("*")) { 213 return true; 214 } 215 if (tag.startsWith(range)) { 216 int len = range.length(); 217 if ((tag.length() == len || tag.charAt(len) == '-')) { 218 return true; 219 } 220 } 221 } 222 223 return false; 224 } 225 filterExtended(List<LanguageRange> priorityList, Collection<String> tags)226 private static List<String> filterExtended(List<LanguageRange> priorityList, 227 Collection<String> tags) { 228 int splitIndex = splitRanges(priorityList); 229 List<LanguageRange> nonZeroRanges; 230 List<LanguageRange> zeroRanges; 231 if (splitIndex != -1) { 232 nonZeroRanges = priorityList.subList(0, splitIndex); 233 zeroRanges = priorityList.subList(splitIndex, priorityList.size()); 234 } else { 235 nonZeroRanges = priorityList; 236 zeroRanges = List.of(); 237 } 238 239 List<String> list = new ArrayList<>(); 240 for (LanguageRange lr : nonZeroRanges) { 241 String range = lr.getRange(); 242 if (range.equals("*")) { 243 tags = removeTagsMatchingExtendedZeroRange(zeroRanges, tags); 244 return new ArrayList<String>(tags); 245 } 246 String[] rangeSubtags = range.split("-"); 247 for (String tag : tags) { 248 // change to lowercase for case-insensitive matching 249 String lowerCaseTag = tag.toLowerCase(Locale.ROOT); 250 String[] tagSubtags = lowerCaseTag.split("-"); 251 if (!rangeSubtags[0].equals(tagSubtags[0]) 252 && !rangeSubtags[0].equals("*")) { 253 continue; 254 } 255 256 int rangeIndex = matchFilterExtendedSubtags(rangeSubtags, 257 tagSubtags); 258 if (rangeSubtags.length == rangeIndex 259 && !caseInsensitiveMatch(list, lowerCaseTag) 260 && !shouldIgnoreFilterExtendedMatch(zeroRanges, 261 lowerCaseTag)) { 262 list.add(tag); // preserve the case of the input tag 263 } 264 } 265 } 266 267 return list; 268 } 269 270 /** 271 * Removes the tag(s) which are falling in the extended exclusion range(s) 272 * i.e range(s) with q=0 and returns the updated collection. If the extended 273 * language ranges contains '*' as one of its non zero range then instead of 274 * returning all the tags, remove those which are matching the range with 275 * quality weight q=0. 276 */ removeTagsMatchingExtendedZeroRange( List<LanguageRange> zeroRange, Collection<String> tags)277 private static Collection<String> removeTagsMatchingExtendedZeroRange( 278 List<LanguageRange> zeroRange, Collection<String> tags) { 279 if (zeroRange.isEmpty()) { 280 tags = removeDuplicates(tags); 281 return tags; 282 } 283 284 List<String> matchingTags = new ArrayList<>(); 285 for (String tag : tags) { 286 // change to lowercase for case-insensitive matching 287 String lowerCaseTag = tag.toLowerCase(Locale.ROOT); 288 if (!shouldIgnoreFilterExtendedMatch(zeroRange, lowerCaseTag) 289 && !caseInsensitiveMatch(matchingTags, lowerCaseTag)) { 290 matchingTags.add(tag); // preserve the case of the input tag 291 } 292 } 293 294 return matchingTags; 295 } 296 297 /** 298 * The tag which is falling in the extended exclusion range(s) should 299 * not be considered as the matching tag. Ignores the tag matching with the 300 * non zero range(s), if the tag also matches with one of the extended 301 * exclusion range(s) i.e. range(s) having quality weight q=0 302 */ shouldIgnoreFilterExtendedMatch( List<LanguageRange> zeroRange, String tag)303 private static boolean shouldIgnoreFilterExtendedMatch( 304 List<LanguageRange> zeroRange, String tag) { 305 if (zeroRange.isEmpty()) { 306 return false; 307 } 308 309 String[] tagSubtags = tag.split("-"); 310 for (LanguageRange lr : zeroRange) { 311 String range = lr.getRange(); 312 if (range.equals("*")) { 313 return true; 314 } 315 316 String[] rangeSubtags = range.split("-"); 317 318 if (!rangeSubtags[0].equals(tagSubtags[0]) 319 && !rangeSubtags[0].equals("*")) { 320 continue; 321 } 322 323 int rangeIndex = matchFilterExtendedSubtags(rangeSubtags, 324 tagSubtags); 325 if (rangeSubtags.length == rangeIndex) { 326 return true; 327 } 328 } 329 330 return false; 331 } 332 matchFilterExtendedSubtags(String[] rangeSubtags, String[] tagSubtags)333 private static int matchFilterExtendedSubtags(String[] rangeSubtags, 334 String[] tagSubtags) { 335 int rangeIndex = 1; 336 int tagIndex = 1; 337 338 while (rangeIndex < rangeSubtags.length 339 && tagIndex < tagSubtags.length) { 340 if (rangeSubtags[rangeIndex].equals("*")) { 341 rangeIndex++; 342 } else if (rangeSubtags[rangeIndex] 343 .equals(tagSubtags[tagIndex])) { 344 rangeIndex++; 345 tagIndex++; 346 } else if (tagSubtags[tagIndex].length() == 1 347 && !tagSubtags[tagIndex].equals("*")) { 348 break; 349 } else { 350 tagIndex++; 351 } 352 } 353 return rangeIndex; 354 } 355 lookup(List<LanguageRange> priorityList, Collection<Locale> locales)356 public static Locale lookup(List<LanguageRange> priorityList, 357 Collection<Locale> locales) { 358 if (priorityList.isEmpty() || locales.isEmpty()) { 359 return null; 360 } 361 362 // Create a list of language tags to be matched. 363 List<String> tags = new ArrayList<>(); 364 for (Locale locale : locales) { 365 tags.add(locale.toLanguageTag()); 366 } 367 368 // Look up a language tags. 369 String lookedUpTag = lookupTag(priorityList, tags); 370 371 if (lookedUpTag == null) { 372 return null; 373 } else { 374 return Locale.forLanguageTag(lookedUpTag); 375 } 376 } 377 lookupTag(List<LanguageRange> priorityList, Collection<String> tags)378 public static String lookupTag(List<LanguageRange> priorityList, 379 Collection<String> tags) { 380 if (priorityList.isEmpty() || tags.isEmpty()) { 381 return null; 382 } 383 384 int splitIndex = splitRanges(priorityList); 385 List<LanguageRange> nonZeroRanges; 386 List<LanguageRange> zeroRanges; 387 if (splitIndex != -1) { 388 nonZeroRanges = priorityList.subList(0, splitIndex); 389 zeroRanges = priorityList.subList(splitIndex, priorityList.size()); 390 } else { 391 nonZeroRanges = priorityList; 392 zeroRanges = List.of(); 393 } 394 395 for (LanguageRange lr : nonZeroRanges) { 396 String range = lr.getRange(); 397 398 // Special language range ("*") is ignored in lookup. 399 if (range.equals("*")) { 400 continue; 401 } 402 403 String rangeForRegex = range.replace("*", "\\p{Alnum}*"); 404 while (!rangeForRegex.isEmpty()) { 405 for (String tag : tags) { 406 // change to lowercase for case-insensitive matching 407 String lowerCaseTag = tag.toLowerCase(Locale.ROOT); 408 if (lowerCaseTag.matches(rangeForRegex) 409 && !shouldIgnoreLookupMatch(zeroRanges, lowerCaseTag)) { 410 return tag; // preserve the case of the input tag 411 } 412 } 413 414 // Truncate from the end.... 415 rangeForRegex = truncateRange(rangeForRegex); 416 } 417 } 418 419 return null; 420 } 421 422 /** 423 * The tag which is falling in the exclusion range(s) should not be 424 * considered as the matching tag. Ignores the tag matching with the 425 * non zero range(s), if the tag also matches with one of the exclusion 426 * range(s) i.e. range(s) having quality weight q=0. 427 */ shouldIgnoreLookupMatch(List<LanguageRange> zeroRange, String tag)428 private static boolean shouldIgnoreLookupMatch(List<LanguageRange> zeroRange, 429 String tag) { 430 for (LanguageRange lr : zeroRange) { 431 String range = lr.getRange(); 432 433 // Special language range ("*") is ignored in lookup. 434 if (range.equals("*")) { 435 continue; 436 } 437 438 String rangeForRegex = range.replace("*", "\\p{Alnum}*"); 439 while (!rangeForRegex.isEmpty()) { 440 if (tag.matches(rangeForRegex)) { 441 return true; 442 } 443 // Truncate from the end.... 444 rangeForRegex = truncateRange(rangeForRegex); 445 } 446 } 447 448 return false; 449 } 450 451 /* Truncate the range from end during the lookup match */ truncateRange(String rangeForRegex)452 private static String truncateRange(String rangeForRegex) { 453 int index = rangeForRegex.lastIndexOf('-'); 454 if (index >= 0) { 455 rangeForRegex = rangeForRegex.substring(0, index); 456 457 // if range ends with an extension key, truncate it. 458 index = rangeForRegex.lastIndexOf('-'); 459 if (index >= 0 && index == rangeForRegex.length() - 2) { 460 rangeForRegex 461 = rangeForRegex.substring(0, rangeForRegex.length() - 2); 462 } 463 } else { 464 rangeForRegex = ""; 465 } 466 467 return rangeForRegex; 468 } 469 470 /* Returns the split index of the priority list, if it contains 471 * language range(s) with quality weight as 0 i.e. q=0, else -1 472 */ splitRanges(List<LanguageRange> priorityList)473 private static int splitRanges(List<LanguageRange> priorityList) { 474 int size = priorityList.size(); 475 for (int index = 0; index < size; index++) { 476 LanguageRange range = priorityList.get(index); 477 if (range.getWeight() == 0) { 478 return index; 479 } 480 } 481 482 return -1; // no q=0 range exists 483 } 484 parse(String ranges)485 public static List<LanguageRange> parse(String ranges) { 486 ranges = ranges.replace(" ", "").toLowerCase(Locale.ROOT); 487 if (ranges.startsWith("accept-language:")) { 488 ranges = ranges.substring(16); // delete unnecessary prefix 489 } 490 491 String[] langRanges = ranges.split(","); 492 List<LanguageRange> list = new ArrayList<>(langRanges.length); 493 List<String> tempList = new ArrayList<>(); 494 int numOfRanges = 0; 495 496 for (String range : langRanges) { 497 int index; 498 String r; 499 double w; 500 501 if ((index = range.indexOf(";q=")) == -1) { 502 r = range; 503 w = MAX_WEIGHT; 504 } else { 505 r = range.substring(0, index); 506 index += 3; 507 try { 508 w = Double.parseDouble(range.substring(index)); 509 } 510 catch (Exception e) { 511 throw new IllegalArgumentException("weight=\"" 512 + range.substring(index) 513 + "\" for language range \"" + r + "\""); 514 } 515 516 if (w < MIN_WEIGHT || w > MAX_WEIGHT) { 517 throw new IllegalArgumentException("weight=" + w 518 + " for language range \"" + r 519 + "\". It must be between " + MIN_WEIGHT 520 + " and " + MAX_WEIGHT + "."); 521 } 522 } 523 524 if (!tempList.contains(r)) { 525 LanguageRange lr = new LanguageRange(r, w); 526 index = numOfRanges; 527 for (int j = 0; j < numOfRanges; j++) { 528 if (list.get(j).getWeight() < w) { 529 index = j; 530 break; 531 } 532 } 533 list.add(index, lr); 534 numOfRanges++; 535 tempList.add(r); 536 537 // Check if the range has an equivalent using IANA LSR data. 538 // If yes, add it to the User's Language Priority List as well. 539 540 // aa-XX -> aa-YY 541 String equivalent; 542 if ((equivalent = getEquivalentForRegionAndVariant(r)) != null 543 && !tempList.contains(equivalent)) { 544 list.add(index+1, new LanguageRange(equivalent, w)); 545 numOfRanges++; 546 tempList.add(equivalent); 547 } 548 549 String[] equivalents; 550 if ((equivalents = getEquivalentsForLanguage(r)) != null) { 551 for (String equiv: equivalents) { 552 // aa-XX -> bb-XX(, cc-XX) 553 if (!tempList.contains(equiv)) { 554 list.add(index+1, new LanguageRange(equiv, w)); 555 numOfRanges++; 556 tempList.add(equiv); 557 } 558 559 // bb-XX -> bb-YY(, cc-YY) 560 equivalent = getEquivalentForRegionAndVariant(equiv); 561 if (equivalent != null 562 && !tempList.contains(equivalent)) { 563 list.add(index+1, new LanguageRange(equivalent, w)); 564 numOfRanges++; 565 tempList.add(equivalent); 566 } 567 } 568 } 569 } 570 } 571 572 return list; 573 } 574 575 /** 576 * A faster alternative approach to String.replaceFirst(), if the given 577 * string is a literal String, not a regex. 578 */ replaceFirstSubStringMatch(String range, String substr, String replacement)579 private static String replaceFirstSubStringMatch(String range, 580 String substr, String replacement) { 581 int pos = range.indexOf(substr); 582 if (pos == -1) { 583 return range; 584 } else { 585 return range.substring(0, pos) + replacement 586 + range.substring(pos + substr.length()); 587 } 588 } 589 getEquivalentsForLanguage(String range)590 private static String[] getEquivalentsForLanguage(String range) { 591 String r = range; 592 593 while (!r.isEmpty()) { 594 if (LocaleEquivalentMaps.singleEquivMap.containsKey(r)) { 595 String equiv = LocaleEquivalentMaps.singleEquivMap.get(r); 596 // Return immediately for performance if the first matching 597 // subtag is found. 598 return new String[]{replaceFirstSubStringMatch(range, 599 r, equiv)}; 600 } else if (LocaleEquivalentMaps.multiEquivsMap.containsKey(r)) { 601 String[] equivs = LocaleEquivalentMaps.multiEquivsMap.get(r); 602 String[] result = new String[equivs.length]; 603 for (int i = 0; i < equivs.length; i++) { 604 result[i] = replaceFirstSubStringMatch(range, 605 r, equivs[i]); 606 } 607 return result; 608 } 609 610 // Truncate the last subtag simply. 611 int index = r.lastIndexOf('-'); 612 if (index == -1) { 613 break; 614 } 615 r = r.substring(0, index); 616 } 617 618 return null; 619 } 620 getEquivalentForRegionAndVariant(String range)621 private static String getEquivalentForRegionAndVariant(String range) { 622 int extensionKeyIndex = getExtentionKeyIndex(range); 623 624 for (String subtag : LocaleEquivalentMaps.regionVariantEquivMap.keySet()) { 625 int index; 626 if ((index = range.indexOf(subtag)) != -1) { 627 // Check if the matching text is a valid region or variant. 628 if (extensionKeyIndex != Integer.MIN_VALUE 629 && index > extensionKeyIndex) { 630 continue; 631 } 632 633 int len = index + subtag.length(); 634 if (range.length() == len || range.charAt(len) == '-') { 635 return replaceFirstSubStringMatch(range, subtag, 636 LocaleEquivalentMaps.regionVariantEquivMap 637 .get(subtag)); 638 } 639 } 640 } 641 642 return null; 643 } 644 getExtentionKeyIndex(String s)645 private static int getExtentionKeyIndex(String s) { 646 char[] c = s.toCharArray(); 647 int index = Integer.MIN_VALUE; 648 for (int i = 1; i < c.length; i++) { 649 if (c[i] == '-') { 650 if (i - index == 2) { 651 return index; 652 } else { 653 index = i; 654 } 655 } 656 } 657 return Integer.MIN_VALUE; 658 } 659 mapEquivalents( List<LanguageRange>priorityList, Map<String, List<String>> map)660 public static List<LanguageRange> mapEquivalents( 661 List<LanguageRange>priorityList, 662 Map<String, List<String>> map) { 663 if (priorityList.isEmpty()) { 664 return new ArrayList<>(); // need to return a empty mutable List 665 } 666 if (map == null || map.isEmpty()) { 667 return new ArrayList<LanguageRange>(priorityList); 668 } 669 670 // Create a map, key=originalKey.toLowerCaes(), value=originalKey 671 Map<String, String> keyMap = new HashMap<>(); 672 for (String key : map.keySet()) { 673 keyMap.put(key.toLowerCase(Locale.ROOT), key); 674 } 675 676 List<LanguageRange> list = new ArrayList<>(); 677 for (LanguageRange lr : priorityList) { 678 String range = lr.getRange(); 679 String r = range; 680 boolean hasEquivalent = false; 681 682 while (!r.isEmpty()) { 683 if (keyMap.containsKey(r)) { 684 hasEquivalent = true; 685 List<String> equivalents = map.get(keyMap.get(r)); 686 if (equivalents != null) { 687 int len = r.length(); 688 for (String equivalent : equivalents) { 689 list.add(new LanguageRange(equivalent.toLowerCase(Locale.ROOT) 690 + range.substring(len), 691 lr.getWeight())); 692 } 693 } 694 // Return immediately if the first matching subtag is found. 695 break; 696 } 697 698 // Truncate the last subtag simply. 699 int index = r.lastIndexOf('-'); 700 if (index == -1) { 701 break; 702 } 703 r = r.substring(0, index); 704 } 705 706 if (!hasEquivalent) { 707 list.add(lr); 708 } 709 } 710 711 return list; 712 } 713 LocaleMatcher()714 private LocaleMatcher() {} 715 716 } 717