• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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