1 // Copyright (C) 2008-2012 IBM Corporation and Others. All Rights Reserved. 2 3 package org.unicode.cldr.util; 4 5 import java.util.Iterator; 6 import java.util.Set; 7 import java.util.TreeSet; 8 import java.util.concurrent.Callable; 9 import java.util.concurrent.ConcurrentHashMap; 10 import java.util.concurrent.ExecutionException; 11 12 import com.google.common.cache.Cache; 13 import com.google.common.cache.CacheBuilder; 14 import com.ibm.icu.text.LocaleDisplayNames; 15 import com.ibm.icu.text.Transform; 16 import com.ibm.icu.util.ULocale; 17 18 /** 19 * This class implements a CLDR UTS#35 compliant locale. 20 * It differs from ICU and Java locales in that it is singleton based, and that it is Comparable. 21 * It uses LocaleIDParser to do the heavy lifting of parsing. 22 * 23 * @author srl 24 * @see LocaleIDParser 25 * @see ULocale 26 */ 27 public final class CLDRLocale implements Comparable<CLDRLocale> { 28 private static final boolean DEBUG = false; 29 30 /* 31 * The name of the root locale. This is widely assumed to be "root". 32 */ 33 private static final String ROOT_NAME = "root"; 34 35 public interface NameFormatter { getDisplayName(CLDRLocale cldrLocale)36 String getDisplayName(CLDRLocale cldrLocale); 37 getDisplayName(CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker)38 String getDisplayName(CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker); 39 getDisplayLanguage(CLDRLocale cldrLocale)40 String getDisplayLanguage(CLDRLocale cldrLocale); 41 getDisplayScript(CLDRLocale cldrLocale)42 String getDisplayScript(CLDRLocale cldrLocale); 43 getDisplayVariant(CLDRLocale cldrLocale)44 String getDisplayVariant(CLDRLocale cldrLocale); 45 getDisplayCountry(CLDRLocale cldrLocale)46 String getDisplayCountry(CLDRLocale cldrLocale); 47 } 48 49 public static class SimpleFormatter implements NameFormatter { 50 private LocaleDisplayNames ldn; 51 SimpleFormatter(ULocale displayLocale)52 public SimpleFormatter(ULocale displayLocale) { 53 this.ldn = LocaleDisplayNames.getInstance(displayLocale); 54 } 55 getDisplayNames()56 public LocaleDisplayNames getDisplayNames() { 57 return ldn; 58 } 59 setDisplayNames(LocaleDisplayNames ldn)60 public LocaleDisplayNames setDisplayNames(LocaleDisplayNames ldn) { 61 return this.ldn = ldn; 62 } 63 64 @Override getDisplayVariant(CLDRLocale cldrLocale)65 public String getDisplayVariant(CLDRLocale cldrLocale) { 66 return ldn.variantDisplayName(cldrLocale.getVariant()); 67 } 68 69 @Override getDisplayCountry(CLDRLocale cldrLocale)70 public String getDisplayCountry(CLDRLocale cldrLocale) { 71 return ldn.regionDisplayName(cldrLocale.getCountry()); 72 } 73 74 @Override getDisplayName(CLDRLocale cldrLocale)75 public String getDisplayName(CLDRLocale cldrLocale) { 76 StringBuffer sb = new StringBuffer(); 77 String l = cldrLocale.getLanguage(); 78 String s = cldrLocale.getScript(); 79 String r = cldrLocale.getCountry(); 80 String v = cldrLocale.getVariant(); 81 82 if (l != null && !l.isEmpty()) { 83 sb.append(getDisplayLanguage(cldrLocale)); 84 } else { 85 sb.append("?"); 86 } 87 if ((s != null && !s.isEmpty()) || 88 (r != null && !r.isEmpty()) || 89 (v != null && !v.isEmpty())) { 90 sb.append(" ("); 91 if (s != null && !s.isEmpty()) { 92 sb.append(getDisplayScript(cldrLocale)).append(","); 93 } 94 if (r != null && !r.isEmpty()) { 95 sb.append(getDisplayCountry(cldrLocale)).append(","); 96 } 97 if (v != null && !v.isEmpty()) { 98 sb.append(getDisplayVariant(cldrLocale)).append(","); 99 } 100 sb.replace(sb.length() - 1, sb.length(), ")"); 101 } 102 return sb.toString(); 103 } 104 105 @Override getDisplayScript(CLDRLocale cldrLocale)106 public String getDisplayScript(CLDRLocale cldrLocale) { 107 return ldn.scriptDisplayName(cldrLocale.getScript()); 108 } 109 110 @Override getDisplayLanguage(CLDRLocale cldrLocale)111 public String getDisplayLanguage(CLDRLocale cldrLocale) { 112 return ldn.languageDisplayName(cldrLocale.getLanguage()); 113 } 114 115 @SuppressWarnings("unused") 116 @Override getDisplayName(CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker)117 public String getDisplayName(CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker) { 118 return getDisplayName(cldrLocale); 119 } 120 } 121 122 /** 123 * @author srl 124 * 125 * This formatter will delegate to CLDRFile.getName if a CLDRFile is given, otherwise StandardCodes 126 */ 127 public static class CLDRFormatter extends SimpleFormatter { 128 private FormatBehavior behavior = FormatBehavior.extend; 129 130 private CLDRFile file = null; 131 CLDRFormatter(CLDRFile fromFile)132 public CLDRFormatter(CLDRFile fromFile) { 133 super(CLDRLocale.getInstance(fromFile.getLocaleID()).toULocale()); 134 file = fromFile; 135 } 136 CLDRFormatter(CLDRFile fromFile, FormatBehavior behavior)137 public CLDRFormatter(CLDRFile fromFile, FormatBehavior behavior) { 138 super(CLDRLocale.getInstance(fromFile.getLocaleID()).toULocale()); 139 this.behavior = behavior; 140 file = fromFile; 141 } 142 CLDRFormatter()143 public CLDRFormatter() { 144 super(ULocale.ROOT); 145 } 146 CLDRFormatter(FormatBehavior behavior)147 public CLDRFormatter(FormatBehavior behavior) { 148 super(ULocale.ROOT); 149 this.behavior = behavior; 150 } 151 152 @Override getDisplayVariant(CLDRLocale cldrLocale)153 public String getDisplayVariant(CLDRLocale cldrLocale) { 154 if (file != null) return file.getName("variant", cldrLocale.getVariant()); 155 return tryForBetter(super.getDisplayVariant(cldrLocale), 156 cldrLocale.getVariant()); 157 } 158 159 @Override getDisplayName(CLDRLocale cldrLocale)160 public String getDisplayName(CLDRLocale cldrLocale) { 161 if (file != null) return file.getName(cldrLocale.toDisplayLanguageTag(), true, null); 162 return super.getDisplayName(cldrLocale); 163 } 164 165 @Override getDisplayName(CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker)166 public String getDisplayName(CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker) { 167 if (file != null) return file.getName(cldrLocale.toDisplayLanguageTag(), onlyConstructCompound, altPicker); 168 return super.getDisplayName(cldrLocale); 169 } 170 171 @Override getDisplayScript(CLDRLocale cldrLocale)172 public String getDisplayScript(CLDRLocale cldrLocale) { 173 if (file != null) return file.getName("script", cldrLocale.getScript()); 174 return tryForBetter(super.getDisplayScript(cldrLocale), 175 cldrLocale.getScript()); 176 } 177 178 @Override getDisplayLanguage(CLDRLocale cldrLocale)179 public String getDisplayLanguage(CLDRLocale cldrLocale) { 180 if (file != null) return file.getName("language", cldrLocale.getLanguage()); 181 return tryForBetter(super.getDisplayLanguage(cldrLocale), 182 cldrLocale.getLanguage()); 183 } 184 185 @Override getDisplayCountry(CLDRLocale cldrLocale)186 public String getDisplayCountry(CLDRLocale cldrLocale) { 187 if (file != null) return file.getName("territory", cldrLocale.getCountry()); 188 return tryForBetter(super.getDisplayLanguage(cldrLocale), 189 cldrLocale.getLanguage()); 190 } 191 tryForBetter(String superString, String code)192 private String tryForBetter(String superString, String code) { 193 if (superString.equals(code)) { 194 String fromLst = StandardCodes.make().getData("language", code); 195 if (fromLst != null && !fromLst.equals(code)) { 196 switch (behavior) { 197 case replace: 198 return fromLst; 199 case extend: 200 return superString + " [" + fromLst + "]"; 201 case extendHtml: 202 return superString + " [<i>" + fromLst + "</i>]"; 203 } 204 } 205 } 206 return superString; 207 } 208 } 209 210 public enum FormatBehavior { 211 replace, extend, extendHtml 212 } 213 214 /** 215 * The parent locale id string, or null if no parent 216 */ 217 private String parentId; 218 219 /** 220 * Reference to the parent CLDRLocale. 221 * 222 * It is volatile, and accessed directly only by getParent, 223 * since it uses the double-check idiom for lazy initialization. 224 */ 225 private volatile CLDRLocale parentLocale; 226 227 /** 228 * Cached ICU format locale 229 */ 230 private ULocale ulocale; 231 /** 232 * base name, 'without parameters'. Currently same as fullname. 233 */ 234 private String basename; 235 /** 236 * Full name 237 */ 238 private String fullname; 239 /** 240 * The LocaleIDParser interprets the various parts (language, country, script, etc). 241 */ 242 private LocaleIDParser parts = null; 243 244 /** 245 * Returns the BCP47 language tag for all except root. For root, returns "root" = ROOT_NAME. 246 * @return 247 */ toDisplayLanguageTag()248 private String toDisplayLanguageTag() { 249 if (getBaseName().equals(ROOT_NAME)) { 250 return ROOT_NAME; 251 } else { 252 return toLanguageTag(); 253 } 254 } 255 256 /** 257 * Return BCP47 language tag 258 * @return 259 */ toLanguageTag()260 public String toLanguageTag() { 261 return ulocale.toLanguageTag(); 262 } 263 264 /** 265 * Return BCP47 languageTag, using special rules for root 266 * @param locale 267 * @return 268 */ toLanguageTag(final String locale)269 public static String toLanguageTag(final String locale) { 270 return getInstance(locale).toLanguageTag(); 271 } 272 273 /** 274 * Construct a CLDRLocale from a string with the full locale ID. 275 * Internal, called by the factory function. 276 * 277 * @param str the string representing a locale. 278 * 279 * If str is empty, it's equal to ULocale.ROOT.getBaseName(), and we are 280 * initializing a CLDRLocale for root. 281 */ CLDRLocale(String str)282 private CLDRLocale(String str) { 283 str = process(str); 284 if (rootMatches(str)) { 285 fullname = ROOT_NAME; 286 parentId = null; 287 } else { 288 parts = new LocaleIDParser(); 289 parts.set(str); 290 fullname = parts.toString(); 291 parentId = LocaleIDParser.getParent(str); // Note, this does now handle explicit parentLocales 292 if (DEBUG) System.out.println(str + " par = " + parentId); 293 } 294 basename = fullname; 295 if (ulocale == null) { 296 ulocale = new ULocale(fullname); 297 } 298 } 299 300 /** 301 * Return the full locale name, in CLDR format. 302 */ 303 @Override toString()304 public String toString() { 305 return fullname; 306 } 307 308 /** 309 * Return the base locale name, in CLDR format, without any @keywords 310 * 311 * @return 312 */ getBaseName()313 public String getBaseName() { 314 return basename; 315 } 316 317 /** 318 * internal: process a string from ICU to CLDR form. For now, just collapse double underscores. 319 * 320 * @param baseName 321 * @return 322 * @internal 323 */ process(String baseName)324 private String process(String baseName) { 325 return baseName.replaceAll("__", "_"); 326 } 327 328 /** 329 * Compare to another CLDRLocale. Uses string order of toString(). 330 */ 331 @Override compareTo(CLDRLocale o)332 public int compareTo(CLDRLocale o) { 333 if (o == this) return 0; 334 return fullname.compareTo(o.fullname); 335 } 336 337 /** 338 * Hashcode - is the hashcode of the full string 339 */ 340 @Override hashCode()341 public int hashCode() { 342 return fullname.hashCode(); 343 } 344 345 /** 346 * Convert to an ICU compatible ULocale. 347 * 348 * @return 349 */ toULocale()350 public ULocale toULocale() { 351 return ulocale; 352 } 353 354 /** 355 * Allocate a CLDRLocale (could be a singleton). If null is passed in, null will be returned. 356 * 357 * @param s 358 * @return 359 */ getInstance(String s)360 public static CLDRLocale getInstance(String s) { 361 if (s == null) { 362 return null; 363 } 364 /* 365 * Normalize variations of ROOT_NAME before checking stringToLoc. 366 */ 367 if (rootMatches(s)) { 368 s = ROOT_NAME; 369 } 370 return stringToLoc.computeIfAbsent(s, k -> new CLDRLocale(k)); 371 } 372 373 /** 374 * Does the given string match the root locale? Treat empty string as matching, 375 * for compatibility with ULocale.ROOT (which is NOT the same as CLDRLocale.ROOT). 376 * Also, ignore case, so "RooT" matches. 377 * 378 * @param s the string 379 * @return true if the string matches ROOT_NAME, else false 380 */ rootMatches(String s)381 private static boolean rootMatches(String s) { 382 /* 383 * Important: 384 * ULocale.ROOT.getBaseName() is "", the empty string, not ROOT_NAME = "root". 385 * CLDRLocale.ROOT.getBaseName() is ROOT_NAME. 386 */ 387 return s.equals(ULocale.ROOT.getBaseName()) || s.equalsIgnoreCase(ROOT_NAME); 388 } 389 390 /** 391 * Public factory function. Allocate a CLDRLocale (could be a singleton). If null is passed in, null will be 392 * returned. 393 * 394 * @param u the ULocale 395 * @return the CLDRLocale 396 */ getInstance(ULocale u)397 public static CLDRLocale getInstance(ULocale u) { 398 if (u == null) { 399 return null; 400 } 401 return getInstance(u.getBaseName()); 402 } 403 404 private static ConcurrentHashMap<String, CLDRLocale> stringToLoc = new ConcurrentHashMap<>(); 405 406 /** 407 * Return the parent locale of this item. Null if no parent (root has no parent) 408 * 409 * @return the parent locale, or null 410 * 411 * Use lazy initialization for parentLocale, since getInstance calling itself 412 * recursively for the parent could cause ConcurrentHashMap to hang within computeIfAbsent. 413 * 414 * Use the "double-check idiom with a volatile field" for high-performance thread-safe 415 * lazy initialization: 416 * https://www.oracle.com/technical-resources/articles/javase/bloch-effective-08-qa.html 417 * 418 * For further efficiency, return null immediately if parentId is null. 419 */ getParent()420 public CLDRLocale getParent() { 421 if (parentId == null) { 422 return null; 423 } 424 CLDRLocale result = parentLocale; 425 if (result == null) { 426 synchronized(this) { 427 result = parentLocale; 428 if (result == null) { 429 parentLocale = result = CLDRLocale.getInstance(parentId); 430 } 431 } 432 } 433 return result; 434 } 435 436 /** 437 * Returns true if other is equal to or is an ancestor of this, false otherwise 438 */ childOf(CLDRLocale other)439 public boolean childOf(CLDRLocale other) { 440 if (other == null) return false; 441 if (other == this) return true; 442 CLDRLocale parent = getParent(); 443 if (parent == null) return false; // end 444 return parent.childOf(other); 445 } 446 447 /** 448 * Return an iterator that will iterate over locale, parent, parent etc, finally reaching root. 449 * 450 * @return 451 */ getParentIterator()452 public Iterable<CLDRLocale> getParentIterator() { 453 final CLDRLocale newThis = this; 454 return new Iterable<CLDRLocale>() { 455 @Override 456 public Iterator<CLDRLocale> iterator() { 457 return new Iterator<CLDRLocale>() { 458 CLDRLocale what = newThis; 459 460 @Override 461 public boolean hasNext() { 462 return what.getParent() != null; 463 } 464 465 @Override 466 public CLDRLocale next() { 467 CLDRLocale curr = what; 468 if (what != null) { 469 what = what.getParent(); 470 } 471 return curr; 472 } 473 474 @Override 475 public void remove() { 476 throw new InternalError("unmodifiable iterator"); 477 } 478 479 }; 480 } 481 }; 482 } 483 484 /** 485 * Get the 'language' locale, as an object. Might be 'this'. 486 * @return 487 */ 488 public CLDRLocale getLanguageLocale() { 489 return getInstance(getLanguage()); 490 } 491 492 public String getLanguage() { 493 return parts == null ? fullname : parts.getLanguage(); 494 } 495 496 public String getScript() { 497 return parts == null ? null : parts.getScript(); 498 } 499 500 public boolean isLanguageLocale() { 501 return this.equals(getLanguageLocale()); 502 } 503 504 /** 505 * Return the region 506 * 507 * @return 508 */ 509 public String getCountry() { 510 return parts == null ? null : parts.getRegion(); 511 } 512 513 /** 514 * Return "the" variant. 515 * 516 * @return 517 */ 518 public String getVariant() { 519 return toULocale().getVariant(); // TODO: replace with parts? 520 } 521 522 /** 523 * Most objects should be singletons, and so equality/inequality comparison is done first. 524 */ 525 @Override 526 public boolean equals(Object o) { 527 if (o == this) return true; 528 if (!(o instanceof CLDRLocale)) return false; 529 return (0 == compareTo((CLDRLocale) o)); 530 } 531 532 /** 533 * The root locale, a singleton. 534 */ 535 public static final CLDRLocale ROOT = getInstance(ULocale.ROOT); 536 537 public String getDisplayName() { 538 return getDisplayName(getDefaultFormatter()); 539 } 540 541 public String getDisplayRegion() { 542 return getDisplayCountry(getDefaultFormatter()); 543 } 544 545 public String getDisplayVariant() { 546 return getDisplayVariant(getDefaultFormatter()); 547 } 548 549 public String getDisplayName(boolean combined, Transform<String, String> picker) { 550 return getDisplayName(getDefaultFormatter(), combined, picker); 551 } 552 553 /** 554 * These functions wrap calls to the displayLocale, but are provided to supply an interface that looks similar to 555 * ULocale.getDisplay___(displayLocale) 556 * 557 * @param displayLocale 558 * @return 559 */ 560 public String getDisplayName(NameFormatter displayLocale) { 561 if (displayLocale == null) displayLocale = getDefaultFormatter(); 562 return displayLocale.getDisplayName(this); 563 } 564 565 // private static LruMap<ULocale, NameFormatter> defaultFormatters = new LruMap<ULocale, NameFormatter>(1); 566 private static Cache<ULocale, NameFormatter> defaultFormatters = CacheBuilder.newBuilder().initialCapacity(1).build(); 567 private static NameFormatter gDefaultFormatter = getSimpleFormatterFor(ULocale.getDefault()); 568 569 public static NameFormatter getSimpleFormatterFor(ULocale loc) { 570 // NameFormatter nf = defaultFormatters.get(loc); 571 // if (nf == null) { 572 // nf = new SimpleFormatter(loc); 573 // defaultFormatters.put(loc, nf); 574 // } 575 // return nf; 576 // return defaultFormatters.getIfPresent(loc); 577 final ULocale uLocFinal = loc; 578 try { 579 return defaultFormatters.get(loc, new Callable<NameFormatter>() { 580 581 @Override 582 public NameFormatter call() throws Exception { 583 return new SimpleFormatter(uLocFinal); 584 } 585 }); 586 } catch (ExecutionException e) { 587 e.printStackTrace(); 588 return null; 589 } 590 } 591 592 public String getDisplayName(ULocale displayLocale) { 593 return getSimpleFormatterFor(displayLocale).getDisplayName(this); 594 } 595 596 public static NameFormatter getDefaultFormatter() { 597 return gDefaultFormatter; 598 } 599 600 public static NameFormatter setDefaultFormatter(NameFormatter nf) { 601 return gDefaultFormatter = nf; 602 } 603 604 /** 605 * These functions wrap calls to the displayLocale, but are provided to supply an interface that looks similar to 606 * ULocale.getDisplay___(displayLocale) 607 * 608 * @param displayLocale 609 * @return 610 */ 611 public String getDisplayCountry(NameFormatter displayLocale) { 612 if (displayLocale == null) displayLocale = getDefaultFormatter(); 613 return displayLocale.getDisplayCountry(this); 614 } 615 616 /** 617 * These functions wrap calls to the displayLocale, but are provided to supply an interface that looks similar to 618 * ULocale.getDisplay___(displayLocale) 619 * 620 * @param displayLocale 621 * @return 622 */ 623 public String getDisplayVariant(NameFormatter displayLocale) { 624 if (displayLocale == null) displayLocale = getDefaultFormatter(); 625 return displayLocale.getDisplayVariant(this); 626 } 627 628 /** 629 * Construct an instance from an array 630 * 631 * @param available 632 * @return 633 */ 634 public static Set<CLDRLocale> getInstance(Iterable<String> available) { 635 Set<CLDRLocale> s = new TreeSet<>(); 636 for (String str : available) { 637 s.add(CLDRLocale.getInstance(str)); 638 } 639 return s; 640 } 641 642 public interface SublocaleProvider { 643 public Set<CLDRLocale> subLocalesOf(CLDRLocale forLocale); 644 } 645 646 public String getDisplayName(NameFormatter engFormat, boolean combined, Transform<String, String> picker) { 647 return engFormat.getDisplayName(this, combined, picker); 648 } 649 650 /** 651 * Return the highest parent that is a child of root, or null. 652 * @return highest parent, or null. ROOT.getHighestNonrootParent() also returns null. 653 */ 654 public CLDRLocale getHighestNonrootParent() { 655 CLDRLocale res; 656 if (this == ROOT) { 657 res = null; 658 } else { 659 CLDRLocale parent = getParent(); 660 if (parent == ROOT || parent == null) { 661 res = this; 662 } else { 663 res = parent.getHighestNonrootParent(); 664 } 665 } 666 if (DEBUG) System.out.println(this + ".HNRP=" + res); 667 return res; 668 } 669 670 public boolean isParentRoot() { 671 return CLDRLocale.ROOT == getParent(); 672 } 673 } 674