1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html#License 3 /* 4 ******************************************************************************* 5 * Copyright (C) 2008-2016, International Business Machines Corporation and 6 * others. All Rights Reserved. 7 ******************************************************************************* 8 */ 9 package com.ibm.icu.impl; 10 11 import java.text.ParseException; 12 import java.util.Collections; 13 import java.util.HashMap; 14 import java.util.Iterator; 15 import java.util.Map; 16 import java.util.MissingResourceException; 17 import java.util.Set; 18 import java.util.TreeMap; 19 20 import com.ibm.icu.text.PluralRanges; 21 import com.ibm.icu.text.PluralRules; 22 import com.ibm.icu.text.PluralRules.PluralType; 23 import com.ibm.icu.util.ULocale; 24 import com.ibm.icu.util.UResourceBundle; 25 26 /** 27 * Loader for plural rules data. 28 */ 29 public class PluralRulesLoader extends PluralRules.Factory { 30 private final Map<String, PluralRules> rulesIdToRules; 31 // lazy init, use getLocaleIdToRulesIdMap to access 32 private Map<String, String> localeIdToCardinalRulesId; 33 private Map<String, String> localeIdToOrdinalRulesId; 34 private Map<String, ULocale> rulesIdToEquivalentULocale; 35 private static Map<String, PluralRanges> localeIdToPluralRanges; 36 37 38 /** 39 * Access through singleton. 40 */ PluralRulesLoader()41 private PluralRulesLoader() { 42 rulesIdToRules = new HashMap<String, PluralRules>(); 43 } 44 45 /** 46 * Returns the locales for which we have plurals data. Utility for testing. 47 */ getAvailableULocales()48 public ULocale[] getAvailableULocales() { 49 Set<String> keys = getLocaleIdToRulesIdMap(PluralType.CARDINAL).keySet(); 50 ULocale[] locales = new ULocale[keys.size()]; 51 int n = 0; 52 for (Iterator<String> iter = keys.iterator(); iter.hasNext();) { 53 locales[n++] = ULocale.createCanonical(iter.next()); 54 } 55 return locales; 56 } 57 58 /** 59 * Returns the functionally equivalent locale. 60 */ getFunctionalEquivalent(ULocale locale, boolean[] isAvailable)61 public ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) { 62 if (isAvailable != null && isAvailable.length > 0) { 63 String localeId = ULocale.canonicalize(locale.getBaseName()); 64 Map<String, String> idMap = getLocaleIdToRulesIdMap(PluralType.CARDINAL); 65 isAvailable[0] = idMap.containsKey(localeId); 66 } 67 68 String rulesId = getRulesIdForLocale(locale, PluralType.CARDINAL); 69 if (rulesId == null || rulesId.trim().length() == 0) { 70 return ULocale.ROOT; // ultimate fallback 71 } 72 73 ULocale result = getRulesIdToEquivalentULocaleMap().get( 74 rulesId); 75 if (result == null) { 76 return ULocale.ROOT; // ultimate fallback 77 } 78 79 return result; 80 } 81 82 /** 83 * Returns the lazily-constructed map. 84 */ getLocaleIdToRulesIdMap(PluralType type)85 private Map<String, String> getLocaleIdToRulesIdMap(PluralType type) { 86 checkBuildRulesIdMaps(); 87 return (type == PluralType.CARDINAL) ? localeIdToCardinalRulesId : localeIdToOrdinalRulesId; 88 } 89 90 /** 91 * Returns the lazily-constructed map. 92 */ getRulesIdToEquivalentULocaleMap()93 private Map<String, ULocale> getRulesIdToEquivalentULocaleMap() { 94 checkBuildRulesIdMaps(); 95 return rulesIdToEquivalentULocale; 96 } 97 98 /** 99 * Lazily constructs the localeIdToRulesId and rulesIdToEquivalentULocale 100 * maps if necessary. These exactly reflect the contents of the locales 101 * resource in plurals.res. 102 */ checkBuildRulesIdMaps()103 private void checkBuildRulesIdMaps() { 104 boolean haveMap; 105 synchronized (this) { 106 haveMap = localeIdToCardinalRulesId != null; 107 } 108 if (!haveMap) { 109 Map<String, String> tempLocaleIdToCardinalRulesId; 110 Map<String, String> tempLocaleIdToOrdinalRulesId; 111 Map<String, ULocale> tempRulesIdToEquivalentULocale; 112 try { 113 UResourceBundle pluralb = getPluralBundle(); 114 // Read cardinal-number rules. 115 UResourceBundle localeb = pluralb.get("locales"); 116 117 // sort for convenience of getAvailableULocales 118 tempLocaleIdToCardinalRulesId = new TreeMap<String, String>(); 119 // not visible 120 tempRulesIdToEquivalentULocale = new HashMap<String, ULocale>(); 121 122 for (int i = 0; i < localeb.getSize(); ++i) { 123 UResourceBundle b = localeb.get(i); 124 String id = b.getKey(); 125 String value = b.getString().intern(); 126 tempLocaleIdToCardinalRulesId.put(id, value); 127 128 if (!tempRulesIdToEquivalentULocale.containsKey(value)) { 129 tempRulesIdToEquivalentULocale.put(value, new ULocale(id)); 130 } 131 } 132 133 // Read ordinal-number rules. 134 localeb = pluralb.get("locales_ordinals"); 135 tempLocaleIdToOrdinalRulesId = new TreeMap<String, String>(); 136 for (int i = 0; i < localeb.getSize(); ++i) { 137 UResourceBundle b = localeb.get(i); 138 String id = b.getKey(); 139 String value = b.getString().intern(); 140 tempLocaleIdToOrdinalRulesId.put(id, value); 141 } 142 } catch (MissingResourceException e) { 143 // dummy so we don't try again 144 tempLocaleIdToCardinalRulesId = Collections.emptyMap(); 145 tempLocaleIdToOrdinalRulesId = Collections.emptyMap(); 146 tempRulesIdToEquivalentULocale = Collections.emptyMap(); 147 } 148 149 synchronized(this) { 150 if (localeIdToCardinalRulesId == null) { 151 localeIdToCardinalRulesId = tempLocaleIdToCardinalRulesId; 152 localeIdToOrdinalRulesId = tempLocaleIdToOrdinalRulesId; 153 rulesIdToEquivalentULocale = tempRulesIdToEquivalentULocale; 154 } 155 } 156 } 157 } 158 159 /** 160 * Gets the rulesId from the locale,with locale fallback. If there is no 161 * rulesId, return null. The rulesId might be the empty string if the rule 162 * is the default rule. 163 */ getRulesIdForLocale(ULocale locale, PluralType type)164 public String getRulesIdForLocale(ULocale locale, PluralType type) { 165 Map<String, String> idMap = getLocaleIdToRulesIdMap(type); 166 String localeId = ULocale.canonicalize(locale.getBaseName()); 167 String rulesId = null; 168 while (null == (rulesId = idMap.get(localeId))) { 169 int ix = localeId.lastIndexOf("_"); 170 if (ix == -1) { 171 break; 172 } 173 localeId = localeId.substring(0, ix); 174 } 175 return rulesId; 176 } 177 178 /** 179 * Gets the rule from the rulesId. If there is no rule for this rulesId, 180 * return null. 181 */ getRulesForRulesId(String rulesId)182 public PluralRules getRulesForRulesId(String rulesId) { 183 // synchronize on the map. release the lock temporarily while we build the rules. 184 PluralRules rules = null; 185 boolean hasRules; // Separate boolean because stored rules can be null. 186 synchronized (rulesIdToRules) { 187 hasRules = rulesIdToRules.containsKey(rulesId); 188 if (hasRules) { 189 rules = rulesIdToRules.get(rulesId); // can be null 190 } 191 } 192 if (!hasRules) { 193 try { 194 UResourceBundle pluralb = getPluralBundle(); 195 UResourceBundle rulesb = pluralb.get("rules"); 196 UResourceBundle setb = rulesb.get(rulesId); 197 198 StringBuilder sb = new StringBuilder(); 199 for (int i = 0; i < setb.getSize(); ++i) { 200 UResourceBundle b = setb.get(i); 201 if (i > 0) { 202 sb.append("; "); 203 } 204 sb.append(b.getKey()); 205 sb.append(": "); 206 sb.append(b.getString()); 207 } 208 rules = PluralRules.parseDescription(sb.toString()); 209 } catch (ParseException e) { 210 } catch (MissingResourceException e) { 211 } 212 synchronized (rulesIdToRules) { 213 if (rulesIdToRules.containsKey(rulesId)) { 214 rules = rulesIdToRules.get(rulesId); 215 } else { 216 rulesIdToRules.put(rulesId, rules); // can be null 217 } 218 } 219 } 220 return rules; 221 } 222 223 /** 224 * Return the plurals resource. Note MissingResourceException is unchecked, 225 * listed here for clarity. Callers should handle this exception. 226 */ getPluralBundle()227 public UResourceBundle getPluralBundle() throws MissingResourceException { 228 return ICUResourceBundle.getBundleInstance( 229 ICUData.ICU_BASE_NAME, "plurals", 230 ICUResourceBundle.ICU_DATA_CLASS_LOADER, true); 231 } 232 233 /** 234 * Returns the plural rules for the the locale. If we don't have data, 235 * com.ibm.icu.text.PluralRules.DEFAULT is returned. 236 */ forLocale(ULocale locale, PluralRules.PluralType type)237 public PluralRules forLocale(ULocale locale, PluralRules.PluralType type) { 238 String rulesId = getRulesIdForLocale(locale, type); 239 if (rulesId == null || rulesId.trim().length() == 0) { 240 return PluralRules.DEFAULT; 241 } 242 PluralRules rules = getRulesForRulesId(rulesId); 243 if (rules == null) { 244 rules = PluralRules.DEFAULT; 245 } 246 return rules; 247 } 248 249 /** 250 * The only instance of the loader. 251 */ 252 public static final PluralRulesLoader loader = new PluralRulesLoader(); 253 254 /* (non-Javadoc) 255 * @see com.ibm.icu.text.PluralRules.Factory#hasOverride(com.ibm.icu.util.ULocale) 256 */ 257 @Override hasOverride(ULocale locale)258 public boolean hasOverride(ULocale locale) { 259 return false; 260 } 261 262 private static final PluralRanges UNKNOWN_RANGE = new PluralRanges().freeze(); 263 getPluralRanges(ULocale locale)264 public PluralRanges getPluralRanges(ULocale locale) { 265 // TODO markdavis Fix the bad fallback, here and elsewhere in this file. 266 String localeId = ULocale.canonicalize(locale.getBaseName()); 267 PluralRanges result; 268 while (null == (result = localeIdToPluralRanges.get(localeId))) { 269 int ix = localeId.lastIndexOf("_"); 270 if (ix == -1) { 271 result = UNKNOWN_RANGE; 272 break; 273 } 274 localeId = localeId.substring(0, ix); 275 } 276 return result; 277 } 278 isPluralRangesAvailable(ULocale locale)279 public boolean isPluralRangesAvailable(ULocale locale) { 280 return getPluralRanges(locale) == UNKNOWN_RANGE; 281 } 282 283 // TODO markdavis FIX HARD-CODED HACK once we have data from CLDR in the bundles 284 static { 285 String[][] pluralRangeData = { 286 {"locales", "id ja km ko lo ms my th vi zh"}, 287 {"other", "other", "other"}, 288 289 {"locales", "am bn fr gu hi hy kn mr pa zu"}, 290 {"one", "one", "one"}, 291 {"one", "other", "other"}, 292 {"other", "other", "other"}, 293 294 {"locales", "fa"}, 295 {"one", "one", "other"}, 296 {"one", "other", "other"}, 297 {"other", "other", "other"}, 298 299 {"locales", "ka"}, 300 {"one", "other", "one"}, 301 {"other", "one", "other"}, 302 {"other", "other", "other"}, 303 304 {"locales", "az de el gl hu it kk ky ml mn ne nl pt sq sw ta te tr ug uz"}, 305 {"one", "other", "other"}, 306 {"other", "one", "one"}, 307 {"other", "other", "other"}, 308 309 {"locales", "af bg ca en es et eu fi nb sv ur"}, 310 {"one", "other", "other"}, 311 {"other", "one", "other"}, 312 {"other", "other", "other"}, 313 314 {"locales", "da fil is"}, 315 {"one", "one", "one"}, 316 {"one", "other", "other"}, 317 {"other", "one", "one"}, 318 {"other", "other", "other"}, 319 320 {"locales", "si"}, 321 {"one", "one", "one"}, 322 {"one", "other", "other"}, 323 {"other", "one", "other"}, 324 {"other", "other", "other"}, 325 326 {"locales", "mk"}, 327 {"one", "one", "other"}, 328 {"one", "other", "other"}, 329 {"other", "one", "other"}, 330 {"other", "other", "other"}, 331 332 {"locales", "lv"}, 333 {"zero", "zero", "other"}, 334 {"zero", "one", "one"}, 335 {"zero", "other", "other"}, 336 {"one", "zero", "other"}, 337 {"one", "one", "one"}, 338 {"one", "other", "other"}, 339 {"other", "zero", "other"}, 340 {"other", "one", "one"}, 341 {"other", "other", "other"}, 342 343 {"locales", "ro"}, 344 {"one", "few", "few"}, 345 {"one", "other", "other"}, 346 {"few", "one", "few"}, 347 {"few", "few", "few"}, 348 {"few", "other", "other"}, 349 {"other", "few", "few"}, 350 {"other", "other", "other"}, 351 352 {"locales", "hr sr bs"}, 353 {"one", "one", "one"}, 354 {"one", "few", "few"}, 355 {"one", "other", "other"}, 356 {"few", "one", "one"}, 357 {"few", "few", "few"}, 358 {"few", "other", "other"}, 359 {"other", "one", "one"}, 360 {"other", "few", "few"}, 361 {"other", "other", "other"}, 362 363 {"locales", "sl"}, 364 {"one", "one", "few"}, 365 {"one", "two", "two"}, 366 {"one", "few", "few"}, 367 {"one", "other", "other"}, 368 {"two", "one", "few"}, 369 {"two", "two", "two"}, 370 {"two", "few", "few"}, 371 {"two", "other", "other"}, 372 {"few", "one", "few"}, 373 {"few", "two", "two"}, 374 {"few", "few", "few"}, 375 {"few", "other", "other"}, 376 {"other", "one", "few"}, 377 {"other", "two", "two"}, 378 {"other", "few", "few"}, 379 {"other", "other", "other"}, 380 381 {"locales", "he"}, 382 {"one", "two", "other"}, 383 {"one", "many", "many"}, 384 {"one", "other", "other"}, 385 {"two", "many", "other"}, 386 {"two", "other", "other"}, 387 {"many", "many", "many"}, 388 {"many", "other", "many"}, 389 {"other", "one", "other"}, 390 {"other", "two", "other"}, 391 {"other", "many", "many"}, 392 {"other", "other", "other"}, 393 394 {"locales", "cs pl sk"}, 395 {"one", "few", "few"}, 396 {"one", "many", "many"}, 397 {"one", "other", "other"}, 398 {"few", "few", "few"}, 399 {"few", "many", "many"}, 400 {"few", "other", "other"}, 401 {"many", "one", "one"}, 402 {"many", "few", "few"}, 403 {"many", "many", "many"}, 404 {"many", "other", "other"}, 405 {"other", "one", "one"}, 406 {"other", "few", "few"}, 407 {"other", "many", "many"}, 408 {"other", "other", "other"}, 409 410 {"locales", "lt ru uk"}, 411 {"one", "one", "one"}, 412 {"one", "few", "few"}, 413 {"one", "many", "many"}, 414 {"one", "other", "other"}, 415 {"few", "one", "one"}, 416 {"few", "few", "few"}, 417 {"few", "many", "many"}, 418 {"few", "other", "other"}, 419 {"many", "one", "one"}, 420 {"many", "few", "few"}, 421 {"many", "many", "many"}, 422 {"many", "other", "other"}, 423 {"other", "one", "one"}, 424 {"other", "few", "few"}, 425 {"other", "many", "many"}, 426 {"other", "other", "other"}, 427 428 {"locales", "cy"}, 429 {"zero", "one", "one"}, 430 {"zero", "two", "two"}, 431 {"zero", "few", "few"}, 432 {"zero", "many", "many"}, 433 {"zero", "other", "other"}, 434 {"one", "two", "two"}, 435 {"one", "few", "few"}, 436 {"one", "many", "many"}, 437 {"one", "other", "other"}, 438 {"two", "few", "few"}, 439 {"two", "many", "many"}, 440 {"two", "other", "other"}, 441 {"few", "many", "many"}, 442 {"few", "other", "other"}, 443 {"many", "other", "other"}, 444 {"other", "one", "one"}, 445 {"other", "two", "two"}, 446 {"other", "few", "few"}, 447 {"other", "many", "many"}, 448 {"other", "other", "other"}, 449 450 {"locales", "ar"}, 451 {"zero", "one", "zero"}, 452 {"zero", "two", "zero"}, 453 {"zero", "few", "few"}, 454 {"zero", "many", "many"}, 455 {"zero", "other", "other"}, 456 {"one", "two", "other"}, 457 {"one", "few", "few"}, 458 {"one", "many", "many"}, 459 {"one", "other", "other"}, 460 {"two", "few", "few"}, 461 {"two", "many", "many"}, 462 {"two", "other", "other"}, 463 {"few", "few", "few"}, 464 {"few", "many", "many"}, 465 {"few", "other", "other"}, 466 {"many", "few", "few"}, 467 {"many", "many", "many"}, 468 {"many", "other", "other"}, 469 {"other", "one", "other"}, 470 {"other", "two", "other"}, 471 {"other", "few", "few"}, 472 {"other", "many", "many"}, 473 {"other", "other", "other"}, 474 }; 475 PluralRanges pr = null; 476 String[] locales = null; 477 HashMap<String, PluralRanges> tempLocaleIdToPluralRanges = new HashMap<String, PluralRanges>(); 478 for (String[] row : pluralRangeData) { 479 if (row[0].equals("locales")) { 480 if (pr != null) { pr.freeze()481 pr.freeze(); 482 for (String locale : locales) { tempLocaleIdToPluralRanges.put(locale, pr)483 tempLocaleIdToPluralRanges.put(locale, pr); 484 } 485 } 486 locales = row[1].split(" "); 487 pr = new PluralRanges(); 488 } else { 489 pr.add( StandardPlural.fromString(row[0])490 StandardPlural.fromString(row[0]), 491 StandardPlural.fromString(row[1]), 492 StandardPlural.fromString(row[2])); 493 } 494 } 495 // do last one 496 for (String locale : locales) { tempLocaleIdToPluralRanges.put(locale, pr)497 tempLocaleIdToPluralRanges.put(locale, pr); 498 } 499 // now make whole thing immutable 500 localeIdToPluralRanges = Collections.unmodifiableMap(tempLocaleIdToPluralRanges); 501 } 502 }