Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/mobile
Path: blob/master/src/java.base/share/classes/sun/util/locale/LocaleMatcher.java
41159 views
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
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
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
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
*/
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
*/
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
*/
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
*/
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
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
*/
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
*/
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
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
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
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
*/
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 */
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
*/
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
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
*/
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
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
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
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
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
714
private LocaleMatcher() {}
715
716
}
717
718