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