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      * Construct a CLDRLocale from a string with the full locale ID.
266      * Internal, called by the factory function.
267      *
268      * @param str the string representing a locale.
269      *
270      * If str is empty, it's equal to ULocale.ROOT.getBaseName(), and we are
271      * initializing a CLDRLocale for root.
272      */
CLDRLocale(String str)273     private CLDRLocale(String str) {
274         str = process(str);
275         if (rootMatches(str)) {
276             fullname = ROOT_NAME;
277             parentId = null;
278         } else {
279             parts = new LocaleIDParser();
280             parts.set(str);
281             fullname = parts.toString();
282             parentId = LocaleIDParser.getParent(str);
283             if (DEBUG) System.out.println(str + " par = " + parentId);
284         }
285         basename = fullname;
286         if (ulocale == null) {
287             ulocale = new ULocale(fullname);
288         }
289     }
290 
291     /**
292      * Return the full locale name, in CLDR format.
293      */
294     @Override
toString()295     public String toString() {
296         return fullname;
297     }
298 
299     /**
300      * Return the base locale name, in CLDR format, without any @keywords
301      *
302      * @return
303      */
getBaseName()304     public String getBaseName() {
305         return basename;
306     }
307 
308     /**
309      * internal: process a string from ICU to CLDR form. For now, just collapse double underscores.
310      *
311      * @param baseName
312      * @return
313      * @internal
314      */
process(String baseName)315     private String process(String baseName) {
316         return baseName.replaceAll("__", "_");
317     }
318 
319     /**
320      * Compare to another CLDRLocale. Uses string order of toString().
321      */
322     @Override
compareTo(CLDRLocale o)323     public int compareTo(CLDRLocale o) {
324         if (o == this) return 0;
325         return fullname.compareTo(o.fullname);
326     }
327 
328     /**
329      * Hashcode - is the hashcode of the full string
330      */
331     @Override
hashCode()332     public int hashCode() {
333         return fullname.hashCode();
334     }
335 
336     /**
337      * Convert to an ICU compatible ULocale.
338      *
339      * @return
340      */
toULocale()341     public ULocale toULocale() {
342         return ulocale;
343     }
344 
345     /**
346      * Allocate a CLDRLocale (could be a singleton). If null is passed in, null will be returned.
347      *
348      * @param s
349      * @return
350      */
getInstance(String s)351     public static CLDRLocale getInstance(String s) {
352         if (s == null) {
353             return null;
354         }
355         /*
356          * Normalize variations of ROOT_NAME before checking stringToLoc.
357          */
358         if (rootMatches(s)) {
359             s = ROOT_NAME;
360         }
361         return stringToLoc.computeIfAbsent(s, k -> new CLDRLocale(k));
362     }
363 
364     /**
365      * Does the given string match the root locale? Treat empty string as matching,
366      * for compatibility with ULocale.ROOT (which is NOT the same as CLDRLocale.ROOT).
367      * Also, ignore case, so "RooT" matches.
368      *
369      * @param s the string
370      * @return true if the string matches ROOT_NAME, else false
371      */
rootMatches(String s)372     private static boolean rootMatches(String s) {
373         /*
374          * Important:
375          * ULocale.ROOT.getBaseName() is "", the empty string, not ROOT_NAME = "root".
376          * CLDRLocale.ROOT.getBaseName() is ROOT_NAME.
377          */
378         return s.equals(ULocale.ROOT.getBaseName()) || s.equalsIgnoreCase(ROOT_NAME);
379     }
380 
381     /**
382      * Public factory function. Allocate a CLDRLocale (could be a singleton). If null is passed in, null will be
383      * returned.
384      *
385      * @param u the ULocale
386      * @return the CLDRLocale
387      */
getInstance(ULocale u)388     public static CLDRLocale getInstance(ULocale u) {
389         if (u == null) {
390             return null;
391         }
392         return getInstance(u.getBaseName());
393     }
394 
395     private static ConcurrentHashMap<String, CLDRLocale> stringToLoc = new ConcurrentHashMap<>();
396 
397     /**
398      * Return the parent locale of this item. Null if no parent (root has no parent)
399      *
400      * @return the parent locale, or null
401      *
402      * Use lazy initialization for parentLocale, since getInstance calling itself
403      * recursively for the parent could cause ConcurrentHashMap to hang within computeIfAbsent.
404      *
405      * Use the "double-check idiom with a volatile field" for high-performance thread-safe
406      * lazy initialization:
407      * https://www.oracle.com/technical-resources/articles/javase/bloch-effective-08-qa.html
408      *
409      * For further efficiency, return null immediately if parentId is null.
410      */
getParent()411     public CLDRLocale getParent() {
412         if (parentId == null) {
413             return null;
414         }
415         CLDRLocale result = parentLocale;
416         if (result == null) {
417             synchronized(this) {
418                 result = parentLocale;
419                 if (result == null) {
420                     parentLocale = result = CLDRLocale.getInstance(parentId);
421                 }
422             }
423         }
424         return result;
425     }
426 
427     /**
428      * Returns true if other is equal to or is an ancestor of this, false otherwise
429      */
childOf(CLDRLocale other)430     public boolean childOf(CLDRLocale other) {
431         if (other == null) return false;
432         if (other == this) return true;
433         CLDRLocale parent = getParent();
434         if (parent == null) return false; // end
435         return parent.childOf(other);
436     }
437 
438     /**
439      * Return an iterator that will iterate over locale, parent, parent etc, finally reaching root.
440      *
441      * @return
442      */
getParentIterator()443     public Iterable<CLDRLocale> getParentIterator() {
444         final CLDRLocale newThis = this;
445         return new Iterable<CLDRLocale>() {
446             @Override
447             public Iterator<CLDRLocale> iterator() {
448                 return new Iterator<CLDRLocale>() {
449                     CLDRLocale what = newThis;
450 
451                     @Override
452                     public boolean hasNext() {
453                         return what.getParent() != null;
454                     }
455 
456                     @Override
457                     public CLDRLocale next() {
458                         CLDRLocale curr = what;
459                         if (what != null) {
460                             what = what.getParent();
461                         }
462                         return curr;
463                     }
464 
465                     @Override
466                     public void remove() {
467                         throw new InternalError("unmodifiable iterator");
468                     }
469 
470                 };
471             }
472         };
473     }
474 
475     /**
476      * Get the 'language' locale, as an object. Might be 'this'.
477      * @return
478      */
479     public CLDRLocale getLanguageLocale() {
480         return getInstance(getLanguage());
481     }
482 
483     public String getLanguage() {
484         return parts == null ? fullname : parts.getLanguage();
485     }
486 
487     public String getScript() {
488         return parts == null ? null : parts.getScript();
489     }
490 
491     public boolean isLanguageLocale() {
492         return this.equals(getLanguageLocale());
493     }
494 
495     /**
496      * Return the region
497      *
498      * @return
499      */
500     public String getCountry() {
501         return parts == null ? null : parts.getRegion();
502     }
503 
504     /**
505      * Return "the" variant.
506      *
507      * @return
508      */
509     public String getVariant() {
510         return toULocale().getVariant(); // TODO: replace with parts?
511     }
512 
513     /**
514      * Most objects should be singletons, and so equality/inequality comparison is done first.
515      */
516     @Override
517     public boolean equals(Object o) {
518         if (o == this) return true;
519         if (!(o instanceof CLDRLocale)) return false;
520         return (0 == compareTo((CLDRLocale) o));
521     }
522 
523     /**
524      * The root locale, a singleton.
525      */
526     public static final CLDRLocale ROOT = getInstance(ULocale.ROOT);
527 
528     public String getDisplayName() {
529         return getDisplayName(getDefaultFormatter());
530     }
531 
532     public String getDisplayRegion() {
533         return getDisplayCountry(getDefaultFormatter());
534     }
535 
536     public String getDisplayVariant() {
537         return getDisplayVariant(getDefaultFormatter());
538     }
539 
540     public String getDisplayName(boolean combined, Transform<String, String> picker) {
541         return getDisplayName(getDefaultFormatter(), combined, picker);
542     }
543 
544     /**
545      * These functions wrap calls to the displayLocale, but are provided to supply an interface that looks similar to
546      * ULocale.getDisplay___(displayLocale)
547      *
548      * @param displayLocale
549      * @return
550      */
551     public String getDisplayName(NameFormatter displayLocale) {
552         if (displayLocale == null) displayLocale = getDefaultFormatter();
553         return displayLocale.getDisplayName(this);
554     }
555 
556 //    private static LruMap<ULocale, NameFormatter> defaultFormatters = new LruMap<ULocale, NameFormatter>(1);
557     private static Cache<ULocale, NameFormatter> defaultFormatters = CacheBuilder.newBuilder().initialCapacity(1).build();
558     private static NameFormatter gDefaultFormatter = getSimpleFormatterFor(ULocale.getDefault());
559 
560     public static NameFormatter getSimpleFormatterFor(ULocale loc) {
561 //        NameFormatter nf = defaultFormatters.get(loc);
562 //        if (nf == null) {
563 //            nf = new SimpleFormatter(loc);
564 //            defaultFormatters.put(loc, nf);
565 //        }
566 //        return nf;
567 //        return defaultFormatters.getIfPresent(loc);
568         final ULocale uLocFinal = loc;
569         try {
570             return defaultFormatters.get(loc, new Callable<NameFormatter>() {
571 
572                 @Override
573                 public NameFormatter call() throws Exception {
574                     return new SimpleFormatter(uLocFinal);
575                 }
576             });
577         } catch (ExecutionException e) {
578             e.printStackTrace();
579             return null;
580         }
581     }
582 
583     public String getDisplayName(ULocale displayLocale) {
584         return getSimpleFormatterFor(displayLocale).getDisplayName(this);
585     }
586 
587     public static NameFormatter getDefaultFormatter() {
588         return gDefaultFormatter;
589     }
590 
591     public static NameFormatter setDefaultFormatter(NameFormatter nf) {
592         return gDefaultFormatter = nf;
593     }
594 
595     /**
596      * These functions wrap calls to the displayLocale, but are provided to supply an interface that looks similar to
597      * ULocale.getDisplay___(displayLocale)
598      *
599      * @param displayLocale
600      * @return
601      */
602     public String getDisplayCountry(NameFormatter displayLocale) {
603         if (displayLocale == null) displayLocale = getDefaultFormatter();
604         return displayLocale.getDisplayCountry(this);
605     }
606 
607     /**
608      * These functions wrap calls to the displayLocale, but are provided to supply an interface that looks similar to
609      * ULocale.getDisplay___(displayLocale)
610      *
611      * @param displayLocale
612      * @return
613      */
614     public String getDisplayVariant(NameFormatter displayLocale) {
615         if (displayLocale == null) displayLocale = getDefaultFormatter();
616         return displayLocale.getDisplayVariant(this);
617     }
618 
619     /**
620      * Construct an instance from an array
621      *
622      * @param available
623      * @return
624      */
625     public static Set<CLDRLocale> getInstance(Iterable<String> available) {
626         Set<CLDRLocale> s = new TreeSet<>();
627         for (String str : available) {
628             s.add(CLDRLocale.getInstance(str));
629         }
630         return s;
631     }
632 
633     public interface SublocaleProvider {
634         public Set<CLDRLocale> subLocalesOf(CLDRLocale forLocale);
635     }
636 
637     public String getDisplayName(NameFormatter engFormat, boolean combined, Transform<String, String> picker) {
638         return engFormat.getDisplayName(this, combined, picker);
639     }
640 
641     /**
642      * Return the highest parent that is a child of root, or null.
643      * @return highest parent, or null.  ROOT.getHighestNonrootParent() also returns null.
644      */
645     public CLDRLocale getHighestNonrootParent() {
646         CLDRLocale res;
647         if (this == ROOT) {
648             res = null;
649         } else {
650             CLDRLocale parent = getParent();
651             if (parent == ROOT || parent == null) {
652                 res = this;
653             } else {
654                 res = parent.getHighestNonrootParent();
655             }
656         }
657         if (DEBUG) System.out.println(this + ".HNRP=" + res);
658         return res;
659     }
660 }
661