1 /*
   2  * Copyright (c) 2012, 2016, 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.Iterator;
  32 import java.util.LinkedHashMap;
  33 import java.util.LinkedList;
  34 import java.util.List;
  35 import java.util.Locale;
  36 import java.util.Locale.*;
  37 import static java.util.Locale.FilteringMode.*;
  38 import static java.util.Locale.LanguageRange.*;
  39 import java.util.Map;
  40 import java.util.Set;
  41 
  42 /**
  43  * Implementation for BCP47 Locale matching
  44  *
  45  */
  46 public final class LocaleMatcher {
  47 
  48     public static List<Locale> filter(List<LanguageRange> priorityList,
  49                                       Collection<Locale> locales,
  50                                       FilteringMode mode) {
  51         if (priorityList.isEmpty() || locales.isEmpty()) {
  52             return new ArrayList<>(); // need to return a empty mutable List
  53         }
  54 
  55         // Create a list of language tags to be matched.
  56         List<String> tags = new ArrayList<>();
  57         for (Locale locale : locales) {
  58             tags.add(locale.toLanguageTag());
  59         }
  60 
  61         // Filter language tags.
  62         List<String> filteredTags = filterTags(priorityList, tags, mode);
  63 
  64         // Create a list of matching locales.
  65         List<Locale> filteredLocales = new ArrayList<>(filteredTags.size());
  66         for (String tag : filteredTags) {
  67               filteredLocales.add(Locale.forLanguageTag(tag));
  68         }
  69 
  70         return filteredLocales;
  71     }
  72 
  73     public static List<String> filterTags(List<LanguageRange> priorityList,
  74                                           Collection<String> tags,
  75                                           FilteringMode mode) {
  76         if (priorityList.isEmpty() || tags.isEmpty()) {
  77             return new ArrayList<>(); // need to return a empty mutable List
  78         }
  79 
  80         ArrayList<LanguageRange> list;
  81         if (mode == EXTENDED_FILTERING) {
  82             return filterExtended(priorityList, tags);
  83         } else {
  84             list = new ArrayList<>();
  85             for (LanguageRange lr : priorityList) {
  86                 String range = lr.getRange();
  87                 if (range.startsWith("*-")
  88                     || range.indexOf("-*") != -1) { // Extended range
  89                     if (mode == AUTOSELECT_FILTERING) {
  90                         return filterExtended(priorityList, tags);
  91                     } else if (mode == MAP_EXTENDED_RANGES) {
  92                         if (range.charAt(0) == '*') {
  93                             range = "*";
  94                         } else {
  95                             range = range.replaceAll("-[*]", "");
  96                         }
  97                         list.add(new LanguageRange(range, lr.getWeight()));
  98                     } else if (mode == REJECT_EXTENDED_RANGES) {
  99                         throw new IllegalArgumentException("An extended range \""
 100                                       + range
 101                                       + "\" found in REJECT_EXTENDED_RANGES mode.");
 102                     }
 103                 } else { // Basic range
 104                     list.add(lr);
 105                 }
 106             }
 107 
 108             return filterBasic(list, tags);
 109         }
 110     }
 111 
 112     private static List<String> filterBasic(List<LanguageRange> priorityList,
 113                                             Collection<String> tags) {
 114         List<String> list = new ArrayList<>();
 115         for (LanguageRange lr : priorityList) {
 116             String range = lr.getRange();
 117             if (range.equals("*")) {
 118                 return new ArrayList<String>(tags);
 119             } else {
 120                 for (String tag : tags) {
 121                     tag = tag.toLowerCase(Locale.ROOT);
 122                     if (tag.startsWith(range)) {
 123                         int len = range.length();
 124                         if ((tag.length() == len || tag.charAt(len) == '-')
 125                             && !list.contains(tag)) {
 126                             list.add(tag);
 127                         }
 128                     }
 129                 }
 130             }
 131         }
 132 
 133         return list;
 134     }
 135 
 136     private static List<String> filterExtended(List<LanguageRange> priorityList,
 137                                                Collection<String> tags) {
 138         List<String> list = new ArrayList<>();
 139         for (LanguageRange lr : priorityList) {
 140             String range = lr.getRange();
 141             if (range.equals("*")) {
 142                 return new ArrayList<String>(tags);
 143             }
 144             String[] rangeSubtags = range.split("-");
 145             for (String tag : tags) {
 146                 tag = tag.toLowerCase(Locale.ROOT);
 147                 String[] tagSubtags = tag.split("-");
 148                 if (!rangeSubtags[0].equals(tagSubtags[0])
 149                     && !rangeSubtags[0].equals("*")) {
 150                     continue;
 151                 }
 152 
 153                 int rangeIndex = 1;
 154                 int tagIndex = 1;
 155 
 156                 while (rangeIndex < rangeSubtags.length
 157                        && tagIndex < tagSubtags.length) {
 158                    if (rangeSubtags[rangeIndex].equals("*")) {
 159                        rangeIndex++;
 160                    } else if (rangeSubtags[rangeIndex].equals(tagSubtags[tagIndex])) {
 161                        rangeIndex++;
 162                        tagIndex++;
 163                    } else if (tagSubtags[tagIndex].length() == 1
 164                               && !tagSubtags[tagIndex].equals("*")) {
 165                        break;
 166                    } else {
 167                        tagIndex++;
 168                    }
 169                }
 170 
 171                if (rangeSubtags.length == rangeIndex && !list.contains(tag)) {
 172                    list.add(tag);
 173                }
 174             }
 175         }
 176 
 177         return list;
 178     }
 179 
 180     public static Locale lookup(List<LanguageRange> priorityList,
 181                                 Collection<Locale> locales) {
 182         if (priorityList.isEmpty() || locales.isEmpty()) {
 183             return null;
 184         }
 185 
 186         // Create a list of language tags to be matched.
 187         List<String> tags = new ArrayList<>();
 188         for (Locale locale : locales) {
 189             tags.add(locale.toLanguageTag());
 190         }
 191 
 192         // Look up a language tags.
 193         String lookedUpTag = lookupTag(priorityList, tags);
 194 
 195         if (lookedUpTag == null) {
 196             return null;
 197         } else {
 198             return Locale.forLanguageTag(lookedUpTag);
 199         }
 200     }
 201 
 202     public static String lookupTag(List<LanguageRange> priorityList,
 203                                    Collection<String> tags) {
 204         if (priorityList.isEmpty() || tags.isEmpty()) {
 205             return null;
 206         }
 207 
 208         for (LanguageRange lr : priorityList) {
 209             String range = lr.getRange();
 210 
 211             // Special language range ("*") is ignored in lookup.
 212             if (range.equals("*")) {
 213                 continue;
 214             }
 215 
 216             String rangeForRegex = range.replaceAll("\\x2A", "\\\\p{Alnum}*");
 217             while (rangeForRegex.length() > 0) {
 218                 for (String tag : tags) {
 219                     tag = tag.toLowerCase(Locale.ROOT);
 220                     if (tag.matches(rangeForRegex)) {
 221                         return tag;
 222                     }
 223                 }
 224 
 225                 // Truncate from the end....
 226                 int index = rangeForRegex.lastIndexOf('-');
 227                 if (index >= 0) {
 228                     rangeForRegex = rangeForRegex.substring(0, index);
 229 
 230                     // if range ends with an extension key, truncate it.
 231                     index = rangeForRegex.lastIndexOf('-');
 232                     if (index >= 0 && index == rangeForRegex.length()-2) {
 233                         rangeForRegex =
 234                             rangeForRegex.substring(0, rangeForRegex.length()-2);
 235                     }
 236                 } else {
 237                     rangeForRegex = "";
 238                 }
 239             }
 240         }
 241 
 242         return null;
 243     }
 244 
 245     public static List<LanguageRange> parse(String ranges) {
 246         ranges = ranges.replaceAll(" ", "").toLowerCase(Locale.ROOT);
 247         if (ranges.startsWith("accept-language:")) {
 248             ranges = ranges.substring(16); // delete unnecessary prefix
 249         }
 250 
 251         String[] langRanges = ranges.split(",");
 252         List<LanguageRange> list = new ArrayList<>(langRanges.length);
 253         List<String> tempList = new ArrayList<>();
 254         int numOfRanges = 0;
 255 
 256         for (String range : langRanges) {
 257             int index;
 258             String r;
 259             double w;
 260 
 261             if ((index = range.indexOf(";q=")) == -1) {
 262                 r = range;
 263                 w = MAX_WEIGHT;
 264             } else {
 265                 r = range.substring(0, index);
 266                 index += 3;
 267                 try {
 268                     w = Double.parseDouble(range.substring(index));
 269                 }
 270                 catch (Exception e) {
 271                     throw new IllegalArgumentException("weight=\""
 272                                   + range.substring(index)
 273                                   + "\" for language range \"" + r + "\"");
 274                 }
 275 
 276                 if (w < MIN_WEIGHT || w > MAX_WEIGHT) {
 277                     throw new IllegalArgumentException("weight=" + w
 278                                   + " for language range \"" + r
 279                                   + "\". It must be between " + MIN_WEIGHT
 280                                   + " and " + MAX_WEIGHT + ".");
 281                 }
 282             }
 283 
 284             if (!tempList.contains(r)) {
 285                 LanguageRange lr = new LanguageRange(r, w);
 286                 index = numOfRanges;
 287                 for (int j = 0; j < numOfRanges; j++) {
 288                     if (list.get(j).getWeight() < w) {
 289                         index = j;
 290                         break;
 291                     }
 292                 }
 293                 list.add(index, lr);
 294                 numOfRanges++;
 295                 tempList.add(r);
 296 
 297                 // Check if the range has an equivalent using IANA LSR data.
 298                 // If yes, add it to the User's Language Priority List as well.
 299 
 300                 // aa-XX -> aa-YY
 301                 String equivalent;
 302                 if ((equivalent = getEquivalentForRegionAndVariant(r)) != null
 303                     && !tempList.contains(equivalent)) {
 304                     list.add(index+1, new LanguageRange(equivalent, w));
 305                     numOfRanges++;
 306                     tempList.add(equivalent);
 307                 }
 308 
 309                 String[] equivalents;
 310                 if ((equivalents = getEquivalentsForLanguage(r)) != null) {
 311                     for (String equiv: equivalents) {
 312                         // aa-XX -> bb-XX(, cc-XX)
 313                         if (!tempList.contains(equiv)) {
 314                             list.add(index+1, new LanguageRange(equiv, w));
 315                             numOfRanges++;
 316                             tempList.add(equiv);
 317                         }
 318 
 319                         // bb-XX -> bb-YY(, cc-YY)
 320                         equivalent = getEquivalentForRegionAndVariant(equiv);
 321                         if (equivalent != null
 322                             && !tempList.contains(equivalent)) {
 323                             list.add(index+1, new LanguageRange(equivalent, w));
 324                             numOfRanges++;
 325                             tempList.add(equivalent);
 326                         }
 327                     }
 328                 }
 329             }
 330         }
 331 
 332         return list;
 333     }
 334 
 335     private static String[] getEquivalentsForLanguage(String range) {
 336         String r = range;
 337 
 338         while (r.length() > 0) {
 339             if (LocaleEquivalentMaps.singleEquivMap.containsKey(r)) {
 340                 String equiv = LocaleEquivalentMaps.singleEquivMap.get(r);
 341                 // Return immediately for performance if the first matching
 342                 // subtag is found.
 343                 return new String[] {range.replaceFirst(r, equiv)};
 344             } else if (LocaleEquivalentMaps.multiEquivsMap.containsKey(r)) {
 345                 String[] equivs = LocaleEquivalentMaps.multiEquivsMap.get(r);
 346                 for (int i = 0; i < equivs.length; i++) {
 347                     equivs[i] = range.replaceFirst(r, equivs[i]);
 348                 }
 349                 return equivs;
 350             }
 351 
 352             // Truncate the last subtag simply.
 353             int index = r.lastIndexOf('-');
 354             if (index == -1) {
 355                 break;
 356             }
 357             r = r.substring(0, index);
 358         }
 359 
 360         return null;
 361     }
 362 
 363     private static String getEquivalentForRegionAndVariant(String range) {
 364         int extensionKeyIndex = getExtentionKeyIndex(range);
 365 
 366         for (String subtag : LocaleEquivalentMaps.regionVariantEquivMap.keySet()) {
 367             int index;
 368             if ((index = range.indexOf(subtag)) != -1) {
 369                 // Check if the matching text is a valid region or variant.
 370                 if (extensionKeyIndex != Integer.MIN_VALUE
 371                     && index > extensionKeyIndex) {
 372                     continue;
 373                 }
 374 
 375                 int len = index + subtag.length();
 376                 if (range.length() == len || range.charAt(len) == '-') {
 377                     return range.replaceFirst(subtag, LocaleEquivalentMaps.regionVariantEquivMap.get(subtag));
 378                 }
 379             }
 380         }
 381 
 382         return null;
 383     }
 384 
 385     private static int getExtentionKeyIndex(String s) {
 386         char[] c = s.toCharArray();
 387         int index = Integer.MIN_VALUE;
 388         for (int i = 1; i < c.length; i++) {
 389             if (c[i] == '-') {
 390                 if (i - index == 2) {
 391                     return index;
 392                 } else {
 393                     index = i;
 394                 }
 395             }
 396         }
 397         return Integer.MIN_VALUE;
 398     }
 399 
 400     public static List<LanguageRange> mapEquivalents(
 401                                           List<LanguageRange>priorityList,
 402                                           Map<String, List<String>> map) {
 403         if (priorityList.isEmpty()) {
 404             return new ArrayList<>(); // need to return a empty mutable List
 405         }
 406         if (map == null || map.isEmpty()) {
 407             return new ArrayList<LanguageRange>(priorityList);
 408         }
 409 
 410         // Create a map, key=originalKey.toLowerCaes(), value=originalKey
 411         Map<String, String> keyMap = new HashMap<>();
 412         for (String key : map.keySet()) {
 413             keyMap.put(key.toLowerCase(Locale.ROOT), key);
 414         }
 415 
 416         List<LanguageRange> list = new ArrayList<>();
 417         for (LanguageRange lr : priorityList) {
 418             String range = lr.getRange();
 419             String r = range;
 420             boolean hasEquivalent = false;
 421 
 422             while (r.length() > 0) {
 423                 if (keyMap.containsKey(r)) {
 424                     hasEquivalent = true;
 425                     List<String> equivalents = map.get(keyMap.get(r));
 426                     if (equivalents != null) {
 427                         int len = r.length();
 428                         for (String equivalent : equivalents) {
 429                             list.add(new LanguageRange(equivalent.toLowerCase(Locale.ROOT)
 430                                      + range.substring(len),
 431                                      lr.getWeight()));
 432                         }
 433                     }
 434                     // Return immediately if the first matching subtag is found.
 435                     break;
 436                 }
 437 
 438                 // Truncate the last subtag simply.
 439                 int index = r.lastIndexOf('-');
 440                 if (index == -1) {
 441                     break;
 442                 }
 443                 r = r.substring(0, index);
 444             }
 445 
 446             if (!hasEquivalent) {
 447                 list.add(lr);
 448             }
 449         }
 450 
 451         return list;
 452     }
 453 
 454     private LocaleMatcher() {}
 455 
 456 }