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