LocaleMatcher.java revision 16027:60837db5d445
139222Sgibbs/* 265942Sgibbs * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. 339222Sgibbs * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 471717Sgibbs * 539222Sgibbs * This code is free software; you can redistribute it and/or modify it 639222Sgibbs * under the terms of the GNU General Public License version 2 only, as 739222Sgibbs * published by the Free Software Foundation. Oracle designates this 839222Sgibbs * particular file as subject to the "Classpath" exception as provided 939222Sgibbs * by Oracle in the LICENSE file that accompanied this code. 1039222Sgibbs * 1139222Sgibbs * This code is distributed in the hope that it will be useful, but WITHOUT 1239222Sgibbs * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 1339222Sgibbs * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 1439222Sgibbs * version 2 for more details (a copy is included in the LICENSE file that 1539222Sgibbs * accompanied this code). 1663457Sgibbs * 1763457Sgibbs * You should have received a copy of the GNU General Public License version 1839222Sgibbs * 2 along with this work; if not, write to the Free Software Foundation, 1939222Sgibbs * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 2039222Sgibbs * 2139222Sgibbs * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 2239222Sgibbs * or visit www.oracle.com if you need additional information or have any 2339222Sgibbs * questions. 2439222Sgibbs */ 2539222Sgibbs 2639222Sgibbspackage sun.util.locale; 2739222Sgibbs 2839222Sgibbsimport java.util.ArrayList; 2939222Sgibbsimport java.util.Collection; 3039222Sgibbsimport java.util.HashMap; 3165942Sgibbsimport java.util.List; 3265942Sgibbsimport java.util.Locale; 3350477Speterimport java.util.Locale.*; 3439222Sgibbsimport static java.util.Locale.FilteringMode.*; 3539222Sgibbsimport static java.util.Locale.LanguageRange.*; 3665942Sgibbsimport java.util.Map; 3739222Sgibbs 3865942Sgibbs/** 3965942Sgibbs * Implementation for BCP47 Locale matching 4039222Sgibbs * 4145969Sgibbs */ 4245969Sgibbspublic final class LocaleMatcher { 4339222Sgibbs 4445969Sgibbs public static List<Locale> filter(List<LanguageRange> priorityList, 4545969Sgibbs Collection<Locale> locales, 4645969Sgibbs FilteringMode mode) { 4745969Sgibbs if (priorityList.isEmpty() || locales.isEmpty()) { 4870204Sgibbs return new ArrayList<>(); // need to return a empty mutable List 4945969Sgibbs } 5045969Sgibbs 5145969Sgibbs // Create a list of language tags to be matched. 5245969Sgibbs List<String> tags = new ArrayList<>(); 5339222Sgibbs for (Locale locale : locales) { 5445969Sgibbs tags.add(locale.toLanguageTag()); 5545969Sgibbs } 5639222Sgibbs 5739222Sgibbs // Filter language tags. 5845969Sgibbs List<String> filteredTags = filterTags(priorityList, tags, mode); 5939222Sgibbs 6045969Sgibbs // Create a list of matching locales. 6170204Sgibbs List<Locale> filteredLocales = new ArrayList<>(filteredTags.size()); 6276634Sgibbs for (String tag : filteredTags) { 6376634Sgibbs filteredLocales.add(Locale.forLanguageTag(tag)); 6445969Sgibbs } 6547275Sgibbs 6647275Sgibbs return filteredLocales; 6747275Sgibbs } 6865942Sgibbs 6947275Sgibbs public static List<String> filterTags(List<LanguageRange> priorityList, 7047275Sgibbs Collection<String> tags, 7147275Sgibbs FilteringMode mode) { 7247275Sgibbs if (priorityList.isEmpty() || tags.isEmpty()) { 7347275Sgibbs return new ArrayList<>(); // need to return a empty mutable List 7447275Sgibbs } 7545969Sgibbs 7639222Sgibbs ArrayList<LanguageRange> list; 7739222Sgibbs if (mode == EXTENDED_FILTERING) { 7845969Sgibbs return filterExtended(priorityList, tags); 7945969Sgibbs } else { 8039222Sgibbs list = new ArrayList<>(); 8165942Sgibbs for (LanguageRange lr : priorityList) { 8265942Sgibbs String range = lr.getRange(); 8365942Sgibbs if (range.startsWith("*-") 8465942Sgibbs || range.indexOf("-*") != -1) { // Extended range 8539222Sgibbs if (mode == AUTOSELECT_FILTERING) { 8647275Sgibbs return filterExtended(priorityList, tags); 8747275Sgibbs } else if (mode == MAP_EXTENDED_RANGES) { 8847275Sgibbs if (range.charAt(0) == '*') { 8939222Sgibbs range = "*"; 9065942Sgibbs } else { 9165942Sgibbs range = range.replaceAll("-[*]", ""); 9265942Sgibbs } 9365942Sgibbs list.add(new LanguageRange(range, lr.getWeight())); 9465942Sgibbs } else if (mode == REJECT_EXTENDED_RANGES) { 9565942Sgibbs throw new IllegalArgumentException("An extended range \"" 9665942Sgibbs + range 9745969Sgibbs + "\" found in REJECT_EXTENDED_RANGES mode."); 9865942Sgibbs } 9970204Sgibbs } else { // Basic range 10065942Sgibbs list.add(lr); 10165942Sgibbs } 10239222Sgibbs } 10371390Sgibbs 10471390Sgibbs return filterBasic(list, tags); 10539222Sgibbs } 10639222Sgibbs } 10749863Sgibbs 10839222Sgibbs private static List<String> filterBasic(List<LanguageRange> priorityList, 10939222Sgibbs Collection<String> tags) { 11039222Sgibbs int splitIndex = splitRanges(priorityList); 11139222Sgibbs List<LanguageRange> nonZeroRanges; 11239222Sgibbs List<LanguageRange> zeroRanges; 11339222Sgibbs if (splitIndex != -1) { 11465942Sgibbs nonZeroRanges = priorityList.subList(0, splitIndex); 11565942Sgibbs zeroRanges = priorityList.subList(splitIndex, priorityList.size()); 11639222Sgibbs } else { 11739222Sgibbs nonZeroRanges = priorityList; 11845969Sgibbs zeroRanges = List.of(); 11945969Sgibbs } 12039222Sgibbs 12145969Sgibbs List<String> list = new ArrayList<>(); 12239222Sgibbs for (LanguageRange lr : nonZeroRanges) { 12365942Sgibbs String range = lr.getRange(); 12465942Sgibbs if (range.equals("*")) { 12565942Sgibbs tags = removeTagsMatchingBasicZeroRange(zeroRanges, tags); 12639222Sgibbs return new ArrayList<String>(tags); 12765942Sgibbs } else { 12839222Sgibbs for (String tag : tags) { 12939222Sgibbs tag = tag.toLowerCase(Locale.ROOT); 13045969Sgibbs if (tag.startsWith(range)) { 13145969Sgibbs int len = range.length(); 13239222Sgibbs if ((tag.length() == len || tag.charAt(len) == '-') 13339222Sgibbs && !list.contains(tag) 13465942Sgibbs && !shouldIgnoreFilterBasicMatch(zeroRanges, tag)) { 13565942Sgibbs list.add(tag); 13655580Sgibbs } 13765942Sgibbs } 13865942Sgibbs } 13965942Sgibbs } 14065942Sgibbs } 14155580Sgibbs 14265942Sgibbs return list; 14365942Sgibbs } 14465942Sgibbs 14565942Sgibbs /** 14666269Sgibbs * Removes the tag(s) which are falling in the basic exclusion range(s) i.e 14765942Sgibbs * range(s) with q=0 and returns the updated collection. If the basic 14870204Sgibbs * language ranges contains '*' as one of its non zero range then instead of 14965942Sgibbs * returning all the tags, remove those which are matching the range with 15065942Sgibbs * quality weight q=0. 15165942Sgibbs */ 15265942Sgibbs private static Collection<String> removeTagsMatchingBasicZeroRange( 15365942Sgibbs List<LanguageRange> zeroRange, Collection<String> tags) { 15465942Sgibbs if (zeroRange.isEmpty()) { 15565942Sgibbs return tags; 15655580Sgibbs } 15763457Sgibbs 15865942Sgibbs List<String> matchingTags = new ArrayList<>(); 15965942Sgibbs for (String tag : tags) { 16063457Sgibbs tag = tag.toLowerCase(Locale.ROOT); 16165942Sgibbs if (!shouldIgnoreFilterBasicMatch(zeroRange, tag)) { 16265942Sgibbs matchingTags.add(tag); 16365942Sgibbs } 16465942Sgibbs } 16565942Sgibbs 16665942Sgibbs return matchingTags; 16765942Sgibbs } 16865942Sgibbs 16965942Sgibbs /** 17065942Sgibbs * The tag which is falling in the basic exclusion range(s) should not 17170204Sgibbs * be considered as the matching tag. Ignores the tag matching with the 17270204Sgibbs * non-zero ranges, if the tag also matches with one of the basic exclusion 17370204Sgibbs * ranges i.e. range(s) having quality weight q=0 17470204Sgibbs */ 17570204Sgibbs private static boolean shouldIgnoreFilterBasicMatch( 17639222Sgibbs List<LanguageRange> zeroRange, String tag) { 17739222Sgibbs if (zeroRange.isEmpty()) { 17839222Sgibbs return false; 17966269Sgibbs } 18065942Sgibbs 18165942Sgibbs for (LanguageRange lr : zeroRange) { 18265942Sgibbs String range = lr.getRange(); 18365942Sgibbs if (range.equals("*")) { 18465942Sgibbs return true; 18565942Sgibbs } 18665942Sgibbs if (tag.startsWith(range)) { 18770204Sgibbs int len = range.length(); 18870204Sgibbs if ((tag.length() == len || tag.charAt(len) == '-')) { 18970204Sgibbs return true; 19070204Sgibbs } 19163457Sgibbs } 19265942Sgibbs } 19365942Sgibbs 19465942Sgibbs return false; 19565942Sgibbs } 19665942Sgibbs 19765942Sgibbs private static List<String> filterExtended(List<LanguageRange> priorityList, 19865942Sgibbs Collection<String> tags) { 19965942Sgibbs int splitIndex = splitRanges(priorityList); 20039222Sgibbs List<LanguageRange> nonZeroRanges; 20147192Sgibbs List<LanguageRange> zeroRanges; 20247192Sgibbs if (splitIndex != -1) { 20347192Sgibbs nonZeroRanges = priorityList.subList(0, splitIndex); 20465942Sgibbs zeroRanges = priorityList.subList(splitIndex, priorityList.size()); 20565942Sgibbs } else { 20650661Sgibbs nonZeroRanges = priorityList; 20765942Sgibbs zeroRanges = List.of(); 20850661Sgibbs } 20965942Sgibbs 21065942Sgibbs List<String> list = new ArrayList<>(); 21165942Sgibbs for (LanguageRange lr : nonZeroRanges) { 21265942Sgibbs String range = lr.getRange(); 21365942Sgibbs if (range.equals("*")) { 21465942Sgibbs tags = removeTagsMatchingExtendedZeroRange(zeroRanges, tags); 21565942Sgibbs return new ArrayList<String>(tags); 21650661Sgibbs } 21750661Sgibbs String[] rangeSubtags = range.split("-"); 21874094Sgibbs for (String tag : tags) { 21974094Sgibbs tag = tag.toLowerCase(Locale.ROOT); 22074094Sgibbs String[] tagSubtags = tag.split("-"); 22174094Sgibbs if (!rangeSubtags[0].equals(tagSubtags[0]) 22274094Sgibbs && !rangeSubtags[0].equals("*")) { 22374094Sgibbs continue; 22474094Sgibbs } 22574094Sgibbs 22674094Sgibbs int rangeIndex = matchFilterExtendedSubtags(rangeSubtags, 22774094Sgibbs tagSubtags); 22874094Sgibbs if (rangeSubtags.length == rangeIndex && !list.contains(tag) 22974094Sgibbs && !shouldIgnoreFilterExtendedMatch(zeroRanges, tag)) { 23074094Sgibbs list.add(tag); 23174094Sgibbs } 23274094Sgibbs } 23374094Sgibbs } 23474094Sgibbs 23574094Sgibbs return list; 23674094Sgibbs } 23774094Sgibbs 23874094Sgibbs /** 23974094Sgibbs * Removes the tag(s) which are falling in the extended exclusion range(s) 24074094Sgibbs * i.e range(s) with q=0 and returns the updated collection. If the extended 24174094Sgibbs * language ranges contains '*' as one of its non zero range then instead of 24274094Sgibbs * returning all the tags, remove those which are matching the range with 24374094Sgibbs * quality weight q=0. 24474094Sgibbs */ 24574094Sgibbs private static Collection<String> removeTagsMatchingExtendedZeroRange( 24674094Sgibbs List<LanguageRange> zeroRange, Collection<String> tags) { 24774094Sgibbs if (zeroRange.isEmpty()) { 24874094Sgibbs return tags; 24974094Sgibbs } 25074094Sgibbs 25174094Sgibbs List<String> matchingTags = new ArrayList<>(); 25274094Sgibbs for (String tag : tags) { 253 tag = tag.toLowerCase(Locale.ROOT); 254 if (!shouldIgnoreFilterExtendedMatch(zeroRange, tag)) { 255 matchingTags.add(tag); 256 } 257 } 258 259 return matchingTags; 260 } 261 262 /** 263 * The tag which is falling in the extended exclusion range(s) should 264 * not be considered as the matching tag. Ignores the tag matching with the 265 * non zero range(s), if the tag also matches with one of the extended 266 * exclusion range(s) i.e. range(s) having quality weight q=0 267 */ 268 private static boolean shouldIgnoreFilterExtendedMatch( 269 List<LanguageRange> zeroRange, String tag) { 270 if (zeroRange.isEmpty()) { 271 return false; 272 } 273 274 String[] tagSubtags = tag.split("-"); 275 for (LanguageRange lr : zeroRange) { 276 String range = lr.getRange(); 277 if (range.equals("*")) { 278 return true; 279 } 280 281 String[] rangeSubtags = range.split("-"); 282 283 if (!rangeSubtags[0].equals(tagSubtags[0]) 284 && !rangeSubtags[0].equals("*")) { 285 continue; 286 } 287 288 int rangeIndex = matchFilterExtendedSubtags(rangeSubtags, 289 tagSubtags); 290 if (rangeSubtags.length == rangeIndex) { 291 return true; 292 } 293 } 294 295 return false; 296 } 297 298 private static int matchFilterExtendedSubtags(String[] rangeSubtags, 299 String[] tagSubtags) { 300 int rangeIndex = 1; 301 int tagIndex = 1; 302 303 while (rangeIndex < rangeSubtags.length 304 && tagIndex < tagSubtags.length) { 305 if (rangeSubtags[rangeIndex].equals("*")) { 306 rangeIndex++; 307 } else if (rangeSubtags[rangeIndex] 308 .equals(tagSubtags[tagIndex])) { 309 rangeIndex++; 310 tagIndex++; 311 } else if (tagSubtags[tagIndex].length() == 1 312 && !tagSubtags[tagIndex].equals("*")) { 313 break; 314 } else { 315 tagIndex++; 316 } 317 } 318 return rangeIndex; 319 } 320 321 public static Locale lookup(List<LanguageRange> priorityList, 322 Collection<Locale> locales) { 323 if (priorityList.isEmpty() || locales.isEmpty()) { 324 return null; 325 } 326 327 // Create a list of language tags to be matched. 328 List<String> tags = new ArrayList<>(); 329 for (Locale locale : locales) { 330 tags.add(locale.toLanguageTag()); 331 } 332 333 // Look up a language tags. 334 String lookedUpTag = lookupTag(priorityList, tags); 335 336 if (lookedUpTag == null) { 337 return null; 338 } else { 339 return Locale.forLanguageTag(lookedUpTag); 340 } 341 } 342 343 public static String lookupTag(List<LanguageRange> priorityList, 344 Collection<String> tags) { 345 if (priorityList.isEmpty() || tags.isEmpty()) { 346 return null; 347 } 348 349 int splitIndex = splitRanges(priorityList); 350 List<LanguageRange> nonZeroRanges; 351 List<LanguageRange> zeroRanges; 352 if (splitIndex != -1) { 353 nonZeroRanges = priorityList.subList(0, splitIndex); 354 zeroRanges = priorityList.subList(splitIndex, priorityList.size()); 355 } else { 356 nonZeroRanges = priorityList; 357 zeroRanges = List.of(); 358 } 359 360 for (LanguageRange lr : nonZeroRanges) { 361 String range = lr.getRange(); 362 363 // Special language range ("*") is ignored in lookup. 364 if (range.equals("*")) { 365 continue; 366 } 367 368 String rangeForRegex = range.replace("*", "\\p{Alnum}*"); 369 while (rangeForRegex.length() > 0) { 370 for (String tag : tags) { 371 tag = tag.toLowerCase(Locale.ROOT); 372 if (tag.matches(rangeForRegex) 373 && !shouldIgnoreLookupMatch(zeroRanges, tag)) { 374 return tag; 375 } 376 } 377 378 // Truncate from the end.... 379 rangeForRegex = truncateRange(rangeForRegex); 380 } 381 } 382 383 return null; 384 } 385 386 /** 387 * The tag which is falling in the exclusion range(s) should not be 388 * considered as the matching tag. Ignores the tag matching with the 389 * non zero range(s), if the tag also matches with one of the exclusion 390 * range(s) i.e. range(s) having quality weight q=0. 391 */ 392 private static boolean shouldIgnoreLookupMatch(List<LanguageRange> zeroRange, 393 String tag) { 394 for (LanguageRange lr : zeroRange) { 395 String range = lr.getRange(); 396 397 // Special language range ("*") is ignored in lookup. 398 if (range.equals("*")) { 399 continue; 400 } 401 402 String rangeForRegex = range.replace("*", "\\p{Alnum}*"); 403 while (rangeForRegex.length() > 0) { 404 if (tag.matches(rangeForRegex)) { 405 return true; 406 } 407 // Truncate from the end.... 408 rangeForRegex = truncateRange(rangeForRegex); 409 } 410 } 411 412 return false; 413 } 414 415 /* Truncate the range from end during the lookup match */ 416 private static String truncateRange(String rangeForRegex) { 417 int index = rangeForRegex.lastIndexOf('-'); 418 if (index >= 0) { 419 rangeForRegex = rangeForRegex.substring(0, index); 420 421 // if range ends with an extension key, truncate it. 422 index = rangeForRegex.lastIndexOf('-'); 423 if (index >= 0 && index == rangeForRegex.length() - 2) { 424 rangeForRegex 425 = rangeForRegex.substring(0, rangeForRegex.length() - 2); 426 } 427 } else { 428 rangeForRegex = ""; 429 } 430 431 return rangeForRegex; 432 } 433 434 /* Returns the split index of the priority list, if it contains 435 * language range(s) with quality weight as 0 i.e. q=0, else -1 436 */ 437 private static int splitRanges(List<LanguageRange> priorityList) { 438 int size = priorityList.size(); 439 for (int index = 0; index < size; index++) { 440 LanguageRange range = priorityList.get(index); 441 if (range.getWeight() == 0) { 442 return index; 443 } 444 } 445 446 return -1; // no q=0 range exists 447 } 448 449 public static List<LanguageRange> parse(String ranges) { 450 ranges = ranges.replace(" ", "").toLowerCase(Locale.ROOT); 451 if (ranges.startsWith("accept-language:")) { 452 ranges = ranges.substring(16); // delete unnecessary prefix 453 } 454 455 String[] langRanges = ranges.split(","); 456 List<LanguageRange> list = new ArrayList<>(langRanges.length); 457 List<String> tempList = new ArrayList<>(); 458 int numOfRanges = 0; 459 460 for (String range : langRanges) { 461 int index; 462 String r; 463 double w; 464 465 if ((index = range.indexOf(";q=")) == -1) { 466 r = range; 467 w = MAX_WEIGHT; 468 } else { 469 r = range.substring(0, index); 470 index += 3; 471 try { 472 w = Double.parseDouble(range.substring(index)); 473 } 474 catch (Exception e) { 475 throw new IllegalArgumentException("weight=\"" 476 + range.substring(index) 477 + "\" for language range \"" + r + "\""); 478 } 479 480 if (w < MIN_WEIGHT || w > MAX_WEIGHT) { 481 throw new IllegalArgumentException("weight=" + w 482 + " for language range \"" + r 483 + "\". It must be between " + MIN_WEIGHT 484 + " and " + MAX_WEIGHT + "."); 485 } 486 } 487 488 if (!tempList.contains(r)) { 489 LanguageRange lr = new LanguageRange(r, w); 490 index = numOfRanges; 491 for (int j = 0; j < numOfRanges; j++) { 492 if (list.get(j).getWeight() < w) { 493 index = j; 494 break; 495 } 496 } 497 list.add(index, lr); 498 numOfRanges++; 499 tempList.add(r); 500 501 // Check if the range has an equivalent using IANA LSR data. 502 // If yes, add it to the User's Language Priority List as well. 503 504 // aa-XX -> aa-YY 505 String equivalent; 506 if ((equivalent = getEquivalentForRegionAndVariant(r)) != null 507 && !tempList.contains(equivalent)) { 508 list.add(index+1, new LanguageRange(equivalent, w)); 509 numOfRanges++; 510 tempList.add(equivalent); 511 } 512 513 String[] equivalents; 514 if ((equivalents = getEquivalentsForLanguage(r)) != null) { 515 for (String equiv: equivalents) { 516 // aa-XX -> bb-XX(, cc-XX) 517 if (!tempList.contains(equiv)) { 518 list.add(index+1, new LanguageRange(equiv, w)); 519 numOfRanges++; 520 tempList.add(equiv); 521 } 522 523 // bb-XX -> bb-YY(, cc-YY) 524 equivalent = getEquivalentForRegionAndVariant(equiv); 525 if (equivalent != null 526 && !tempList.contains(equivalent)) { 527 list.add(index+1, new LanguageRange(equivalent, w)); 528 numOfRanges++; 529 tempList.add(equivalent); 530 } 531 } 532 } 533 } 534 } 535 536 return list; 537 } 538 539 /** 540 * A faster alternative approach to String.replaceFirst(), if the given 541 * string is a literal String, not a regex. 542 */ 543 private static String replaceFirstSubStringMatch(String range, 544 String substr, String replacement) { 545 int pos = range.indexOf(substr); 546 if (pos == -1) { 547 return range; 548 } else { 549 return range.substring(0, pos) + replacement 550 + range.substring(pos + substr.length()); 551 } 552 } 553 554 private static String[] getEquivalentsForLanguage(String range) { 555 String r = range; 556 557 while (r.length() > 0) { 558 if (LocaleEquivalentMaps.singleEquivMap.containsKey(r)) { 559 String equiv = LocaleEquivalentMaps.singleEquivMap.get(r); 560 // Return immediately for performance if the first matching 561 // subtag is found. 562 return new String[]{replaceFirstSubStringMatch(range, 563 r, equiv)}; 564 } else if (LocaleEquivalentMaps.multiEquivsMap.containsKey(r)) { 565 String[] equivs = LocaleEquivalentMaps.multiEquivsMap.get(r); 566 String[] result = new String[equivs.length]; 567 for (int i = 0; i < equivs.length; i++) { 568 result[i] = replaceFirstSubStringMatch(range, 569 r, equivs[i]); 570 } 571 return result; 572 } 573 574 // Truncate the last subtag simply. 575 int index = r.lastIndexOf('-'); 576 if (index == -1) { 577 break; 578 } 579 r = r.substring(0, index); 580 } 581 582 return null; 583 } 584 585 private static String getEquivalentForRegionAndVariant(String range) { 586 int extensionKeyIndex = getExtentionKeyIndex(range); 587 588 for (String subtag : LocaleEquivalentMaps.regionVariantEquivMap.keySet()) { 589 int index; 590 if ((index = range.indexOf(subtag)) != -1) { 591 // Check if the matching text is a valid region or variant. 592 if (extensionKeyIndex != Integer.MIN_VALUE 593 && index > extensionKeyIndex) { 594 continue; 595 } 596 597 int len = index + subtag.length(); 598 if (range.length() == len || range.charAt(len) == '-') { 599 return replaceFirstSubStringMatch(range, subtag, 600 LocaleEquivalentMaps.regionVariantEquivMap 601 .get(subtag)); 602 } 603 } 604 } 605 606 return null; 607 } 608 609 private static int getExtentionKeyIndex(String s) { 610 char[] c = s.toCharArray(); 611 int index = Integer.MIN_VALUE; 612 for (int i = 1; i < c.length; i++) { 613 if (c[i] == '-') { 614 if (i - index == 2) { 615 return index; 616 } else { 617 index = i; 618 } 619 } 620 } 621 return Integer.MIN_VALUE; 622 } 623 624 public static List<LanguageRange> mapEquivalents( 625 List<LanguageRange>priorityList, 626 Map<String, List<String>> map) { 627 if (priorityList.isEmpty()) { 628 return new ArrayList<>(); // need to return a empty mutable List 629 } 630 if (map == null || map.isEmpty()) { 631 return new ArrayList<LanguageRange>(priorityList); 632 } 633 634 // Create a map, key=originalKey.toLowerCaes(), value=originalKey 635 Map<String, String> keyMap = new HashMap<>(); 636 for (String key : map.keySet()) { 637 keyMap.put(key.toLowerCase(Locale.ROOT), key); 638 } 639 640 List<LanguageRange> list = new ArrayList<>(); 641 for (LanguageRange lr : priorityList) { 642 String range = lr.getRange(); 643 String r = range; 644 boolean hasEquivalent = false; 645 646 while (r.length() > 0) { 647 if (keyMap.containsKey(r)) { 648 hasEquivalent = true; 649 List<String> equivalents = map.get(keyMap.get(r)); 650 if (equivalents != null) { 651 int len = r.length(); 652 for (String equivalent : equivalents) { 653 list.add(new LanguageRange(equivalent.toLowerCase(Locale.ROOT) 654 + range.substring(len), 655 lr.getWeight())); 656 } 657 } 658 // Return immediately if the first matching subtag is found. 659 break; 660 } 661 662 // Truncate the last subtag simply. 663 int index = r.lastIndexOf('-'); 664 if (index == -1) { 665 break; 666 } 667 r = r.substring(0, index); 668 } 669 670 if (!hasEquivalent) { 671 list.add(lr); 672 } 673 } 674 675 return list; 676 } 677 678 private LocaleMatcher() {} 679 680} 681