• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html
3 /*
4  *******************************************************************************
5  * Copyright (C) 2009-2016, International Business Machines Corporation and
6  * others. All Rights Reserved.
7  *******************************************************************************
8  */
9 package com.ibm.icu.impl;
10 
11 import java.util.ArrayList;
12 import java.util.Collections;
13 import java.util.Comparator;
14 import java.util.HashMap;
15 import java.util.HashSet;
16 import java.util.Iterator;
17 import java.util.List;
18 import java.util.Locale;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.MissingResourceException;
22 import java.util.Set;
23 
24 import com.ibm.icu.impl.CurrencyData.CurrencyDisplayInfo;
25 import com.ibm.icu.impl.locale.AsciiUtil;
26 import com.ibm.icu.lang.UCharacter;
27 import com.ibm.icu.lang.UScript;
28 import com.ibm.icu.text.BreakIterator;
29 import com.ibm.icu.text.CaseMap;
30 import com.ibm.icu.text.DisplayContext;
31 import com.ibm.icu.text.DisplayContext.Type;
32 import com.ibm.icu.text.LocaleDisplayNames;
33 import com.ibm.icu.util.ULocale;
34 import com.ibm.icu.util.UResourceBundle;
35 
36 public class LocaleDisplayNamesImpl extends LocaleDisplayNames {
37     private final ULocale locale;
38     private final DialectHandling dialectHandling;
39     private final DisplayContext capitalization;
40     private final DisplayContext nameLength;
41     private final DisplayContext substituteHandling;
42     private final DataTable langData;
43     private final DataTable regionData;
44     // Compiled SimpleFormatter patterns.
45     private final String separatorFormat;
46     private final String format;
47     private final String keyTypeFormat;
48     private final char formatOpenParen;
49     private final char formatReplaceOpenParen;
50     private final char formatCloseParen;
51     private final char formatReplaceCloseParen;
52     private final CurrencyDisplayInfo currencyDisplayInfo;
53 
54     private static final Cache cache = new Cache();
55 
56     /**
57      * Capitalization context usage types for locale display names
58      */
59     private enum CapitalizationContextUsage {
60         LANGUAGE,
61         SCRIPT,
62         TERRITORY,
63         VARIANT,
64         KEY,
65         KEYVALUE
66     }
67     /**
68      * Capitalization transforms. For each usage type, indicates whether to titlecase for
69      * the context specified in capitalization (which we know at construction time).
70      */
71     private boolean[] capitalizationUsage = null;
72     /**
73      * Map from resource key to CapitalizationContextUsage value
74      */
75     private static final Map<String, CapitalizationContextUsage> contextUsageTypeMap;
76     static {
77         contextUsageTypeMap=new HashMap<String, CapitalizationContextUsage>();
78         contextUsageTypeMap.put("languages", CapitalizationContextUsage.LANGUAGE);
79         contextUsageTypeMap.put("script",    CapitalizationContextUsage.SCRIPT);
80         contextUsageTypeMap.put("territory", CapitalizationContextUsage.TERRITORY);
81         contextUsageTypeMap.put("variant",   CapitalizationContextUsage.VARIANT);
82         contextUsageTypeMap.put("key",       CapitalizationContextUsage.KEY);
83         contextUsageTypeMap.put("keyValue",  CapitalizationContextUsage.KEYVALUE);
84     }
85     /**
86      * BreakIterator to use for capitalization
87      */
88     private transient BreakIterator capitalizationBrkIter = null;
89 
90     private static final CaseMap.Title TO_TITLE_WHOLE_STRING_NO_LOWERCASE =
91             CaseMap.toTitle().wholeString().noLowercase();
92 
toTitleWholeStringNoLowercase(ULocale locale, String s)93     private static String toTitleWholeStringNoLowercase(ULocale locale, String s) {
94         return TO_TITLE_WHOLE_STRING_NO_LOWERCASE.apply(locale.toLocale(), null, s);
95     }
96 
getInstance(ULocale locale, DialectHandling dialectHandling)97     public static LocaleDisplayNames getInstance(ULocale locale, DialectHandling dialectHandling) {
98         synchronized (cache) {
99             return cache.get(locale, dialectHandling);
100         }
101     }
102 
getInstance(ULocale locale, DisplayContext... contexts)103     public static LocaleDisplayNames getInstance(ULocale locale, DisplayContext... contexts) {
104         synchronized (cache) {
105             return cache.get(locale, contexts);
106         }
107     }
108 
109     private final class CapitalizationContextSink extends UResource.Sink {
110         boolean hasCapitalizationUsage = false;
111 
112         @Override
put(UResource.Key key, UResource.Value value, boolean noFallback)113         public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
114             UResource.Table contextsTable = value.getTable();
115             for (int i = 0; contextsTable.getKeyAndValue(i, key, value); ++i) {
116 
117                 CapitalizationContextUsage usage = contextUsageTypeMap.get(key.toString());
118                 if (usage == null) { continue; };
119 
120                 int[] intVector = value.getIntVector();
121                 if (intVector.length < 2) { continue; }
122 
123                 int titlecaseInt = (capitalization == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU)
124                         ? intVector[0] : intVector[1];
125                 if (titlecaseInt == 0) { continue; }
126 
127                 capitalizationUsage[usage.ordinal()] = true;
128                 hasCapitalizationUsage = true;
129             }
130         }
131     }
132 
LocaleDisplayNamesImpl(ULocale locale, DialectHandling dialectHandling)133     public LocaleDisplayNamesImpl(ULocale locale, DialectHandling dialectHandling) {
134         this(locale, (dialectHandling==DialectHandling.STANDARD_NAMES)? DisplayContext.STANDARD_NAMES: DisplayContext.DIALECT_NAMES,
135                 DisplayContext.CAPITALIZATION_NONE);
136     }
137 
LocaleDisplayNamesImpl(ULocale locale, DisplayContext... contexts)138     public LocaleDisplayNamesImpl(ULocale locale, DisplayContext... contexts) {
139         DialectHandling dialectHandling = DialectHandling.STANDARD_NAMES;
140         DisplayContext capitalization = DisplayContext.CAPITALIZATION_NONE;
141         DisplayContext nameLength = DisplayContext.LENGTH_FULL;
142         DisplayContext substituteHandling = DisplayContext.SUBSTITUTE;
143         for (DisplayContext contextItem : contexts) {
144             switch (contextItem.type()) {
145             case DIALECT_HANDLING:
146                 dialectHandling = (contextItem.value()==DisplayContext.STANDARD_NAMES.value())?
147                         DialectHandling.STANDARD_NAMES: DialectHandling.DIALECT_NAMES;
148                 break;
149             case CAPITALIZATION:
150                 capitalization = contextItem;
151                 break;
152             case DISPLAY_LENGTH:
153                 nameLength = contextItem;
154                 break;
155             case SUBSTITUTE_HANDLING:
156                 substituteHandling = contextItem;
157                 break;
158             default:
159                 break;
160             }
161         }
162 
163         this.dialectHandling = dialectHandling;
164         this.capitalization = capitalization;
165         this.nameLength = nameLength;
166         this.substituteHandling = substituteHandling;
167         this.langData = LangDataTables.impl.get(locale, substituteHandling == DisplayContext.NO_SUBSTITUTE);
168         this.regionData = RegionDataTables.impl.get(locale, substituteHandling == DisplayContext.NO_SUBSTITUTE);
169         this.locale = ULocale.ROOT.equals(langData.getLocale()) ? regionData.getLocale() :
170             langData.getLocale();
171 
172         // Note, by going through DataTable, this uses table lookup rather than straight lookup.
173         // That should get us the same data, I think.  This way we don't have to explicitly
174         // load the bundle again.  Using direct lookup didn't seem to make an appreciable
175         // difference in performance.
176         String sep = langData.get("localeDisplayPattern", "separator");
177         if (sep == null || "separator".equals(sep)) {
178             sep = "{0}, {1}";
179         }
180         StringBuilder sb = new StringBuilder();
181         this.separatorFormat = SimpleFormatterImpl.compileToStringMinMaxArguments(sep, sb, 2, 2);
182 
183         String pattern = langData.get("localeDisplayPattern", "pattern");
184         if (pattern == null || "pattern".equals(pattern)) {
185             pattern = "{0} ({1})";
186         }
187         this.format = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
188         if (pattern.contains("(")) {
189             formatOpenParen = '(';
190             formatCloseParen = ')';
191             formatReplaceOpenParen = '[';
192             formatReplaceCloseParen = ']';
193         } else  {
194             formatOpenParen = '(';
195             formatCloseParen = ')';
196             formatReplaceOpenParen = '[';
197             formatReplaceCloseParen = ']';
198         }
199 
200         String keyTypePattern = langData.get("localeDisplayPattern", "keyTypePattern");
201         if (keyTypePattern == null || "keyTypePattern".equals(keyTypePattern)) {
202             keyTypePattern = "{0}={1}";
203         }
204         this.keyTypeFormat = SimpleFormatterImpl.compileToStringMinMaxArguments(
205                 keyTypePattern, sb, 2, 2);
206 
207         // Get values from the contextTransforms data if we need them
208         // Also check whether we will need a break iterator (depends on the data)
209         boolean needBrkIter = false;
210         if (capitalization == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU ||
211                 capitalization == DisplayContext.CAPITALIZATION_FOR_STANDALONE) {
212             capitalizationUsage = new boolean[CapitalizationContextUsage.values().length]; // initialized to all false
213             ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
214             CapitalizationContextSink sink = new CapitalizationContextSink();
215             try {
216                 rb.getAllItemsWithFallback("contextTransforms", sink);
217             }
218             catch (MissingResourceException e) {
219                 // Silently ignore.  Not every locale has contextTransforms.
220             }
221             needBrkIter = sink.hasCapitalizationUsage;
222         }
223         // Get a sentence break iterator if we will need it
224         if (needBrkIter || capitalization == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE) {
225             capitalizationBrkIter = BreakIterator.getSentenceInstance(locale);
226         }
227 
228         this.currencyDisplayInfo = CurrencyData.provider.getInstance(locale, false);
229     }
230 
231     @Override
getLocale()232     public ULocale getLocale() {
233         return locale;
234     }
235 
236     @Override
getDialectHandling()237     public DialectHandling getDialectHandling() {
238         return dialectHandling;
239     }
240 
241     @Override
getContext(DisplayContext.Type type)242     public DisplayContext getContext(DisplayContext.Type type) {
243         DisplayContext result;
244         switch (type) {
245         case DIALECT_HANDLING:
246             result = (dialectHandling==DialectHandling.STANDARD_NAMES)? DisplayContext.STANDARD_NAMES: DisplayContext.DIALECT_NAMES;
247             break;
248         case CAPITALIZATION:
249             result = capitalization;
250             break;
251         case DISPLAY_LENGTH:
252             result = nameLength;
253             break;
254         case SUBSTITUTE_HANDLING:
255             result = substituteHandling;
256             break;
257         default:
258             result = DisplayContext.STANDARD_NAMES; // hmm, we should do something else here
259             break;
260         }
261         return result;
262     }
263 
adjustForUsageAndContext(CapitalizationContextUsage usage, String name)264     private String adjustForUsageAndContext(CapitalizationContextUsage usage, String name) {
265         if (name != null && name.length() > 0 && UCharacter.isLowerCase(name.codePointAt(0)) &&
266                 (capitalization==DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE ||
267                 (capitalizationUsage != null && capitalizationUsage[usage.ordinal()]) )) {
268             // Note, won't have capitalizationUsage != null && capitalizationUsage[usage.ordinal()]
269             // unless capitalization is CAPITALIZATION_FOR_UI_LIST_OR_MENU or CAPITALIZATION_FOR_STANDALONE
270             synchronized (this) {
271                 if (capitalizationBrkIter == null) {
272                     // should only happen when deserializing, etc.
273                     capitalizationBrkIter = BreakIterator.getSentenceInstance(locale);
274                 }
275                 return UCharacter.toTitleCase(locale, name, capitalizationBrkIter,
276                         UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT);
277             }
278         }
279         return name;
280     }
281 
282     @Override
localeDisplayName(ULocale locale)283     public String localeDisplayName(ULocale locale) {
284         return localeDisplayNameInternal(locale);
285     }
286 
287     @Override
localeDisplayName(Locale locale)288     public String localeDisplayName(Locale locale) {
289         return localeDisplayNameInternal(ULocale.forLocale(locale));
290     }
291 
292     @Override
localeDisplayName(String localeId)293     public String localeDisplayName(String localeId) {
294         return localeDisplayNameInternal(new ULocale(localeId));
295     }
296 
297     // TODO: implement use of capitalization
localeDisplayNameInternal(ULocale locale)298     private String localeDisplayNameInternal(ULocale locale) {
299         // lang
300         // lang (script, country, variant, keyword=value, ...)
301         // script, country, variant, keyword=value, ...
302 
303         String resultName = null;
304 
305         String lang = locale.getLanguage();
306 
307         // Empty basename indicates root locale (keywords are ignored for this).
308         // For the display name, we treat this as unknown language (ICU-20273).
309         if (lang.isEmpty()) {
310             lang = "und";
311         }
312         String script = locale.getScript();
313         String country = locale.getCountry();
314         String variant = locale.getVariant();
315 
316         boolean hasScript = script.length() > 0;
317         boolean hasCountry = country.length() > 0;
318         boolean hasVariant = variant.length() > 0;
319 
320         // always have a value for lang
321         if (dialectHandling == DialectHandling.DIALECT_NAMES) {
322             do { // loop construct is so we can break early out of search
323                 if (hasScript && hasCountry) {
324                     String langScriptCountry = lang + '_' + script + '_' + country;
325                     String result = localeIdName(langScriptCountry);
326                     if (result != null && !result.equals(langScriptCountry)) {
327                         resultName = result;
328                         hasScript = false;
329                         hasCountry = false;
330                         break;
331                     }
332                 }
333                 if (hasScript) {
334                     String langScript = lang + '_' + script;
335                     String result = localeIdName(langScript);
336                     if (result != null && !result.equals(langScript)) {
337                         resultName = result;
338                         hasScript = false;
339                         break;
340                     }
341                 }
342                 if (hasCountry) {
343                     String langCountry = lang + '_' + country;
344                     String result = localeIdName(langCountry);
345                     if (result != null && !result.equals(langCountry)) {
346                         resultName = result;
347                         hasCountry = false;
348                         break;
349                     }
350                 }
351             } while (false);
352         }
353 
354         if (resultName == null) {
355             String result = localeIdName(lang);
356             if (result == null) { return null; }
357             resultName = result
358                     .replace(formatOpenParen, formatReplaceOpenParen)
359                     .replace(formatCloseParen, formatReplaceCloseParen);
360         }
361 
362         StringBuilder buf = new StringBuilder();
363         if (hasScript) {
364             // first element, don't need appendWithSep
365             String result = scriptDisplayNameInContext(script, true);
366             if (result == null) { return null; }
367             buf.append(result
368                     .replace(formatOpenParen, formatReplaceOpenParen)
369                     .replace(formatCloseParen, formatReplaceCloseParen));
370         }
371         if (hasCountry) {
372             String result = regionDisplayName(country, true);
373             if (result == null) { return null; }
374             appendWithSep(result
375                     .replace(formatOpenParen, formatReplaceOpenParen)
376                     .replace(formatCloseParen, formatReplaceCloseParen), buf);
377         }
378         if (hasVariant) {
379             String result = variantDisplayName(variant, true);
380             if (result == null) { return null; }
381             appendWithSep(result
382                     .replace(formatOpenParen, formatReplaceOpenParen)
383                     .replace(formatCloseParen, formatReplaceCloseParen), buf);
384         }
385 
386         Iterator<String> keys = locale.getKeywords();
387         if (keys != null) {
388             while (keys.hasNext()) {
389                 String key = keys.next();
390                 String value = locale.getKeywordValue(key);
391                 String keyDisplayName = keyDisplayName(key, true);
392                 if (keyDisplayName == null) { return null; }
393                 keyDisplayName = keyDisplayName
394                         .replace(formatOpenParen, formatReplaceOpenParen)
395                         .replace(formatCloseParen, formatReplaceCloseParen);
396                 String valueDisplayName = keyValueDisplayName(key, value, true);
397                 if (valueDisplayName == null) { return null; }
398                 valueDisplayName = valueDisplayName
399                         .replace(formatOpenParen, formatReplaceOpenParen)
400                         .replace(formatCloseParen, formatReplaceCloseParen);
401                 if (!valueDisplayName.equals(value)) {
402                     appendWithSep(valueDisplayName, buf);
403                 } else if (!key.equals(keyDisplayName)) {
404                     String keyValue = SimpleFormatterImpl.formatCompiledPattern(
405                             keyTypeFormat, keyDisplayName, valueDisplayName);
406                     appendWithSep(keyValue, buf);
407                 } else {
408                     appendWithSep(keyDisplayName, buf)
409                     .append("=")
410                     .append(valueDisplayName);
411                 }
412             }
413         }
414 
415         String resultRemainder = null;
416         if (buf.length() > 0) {
417             resultRemainder = buf.toString();
418         }
419 
420         if (resultRemainder != null) {
421             resultName = SimpleFormatterImpl.formatCompiledPattern(
422                     format, resultName, resultRemainder);
423         }
424 
425         return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, resultName);
426     }
427 
localeIdName(String localeId)428     private String localeIdName(String localeId) {
429         String locIdName;
430         if (nameLength == DisplayContext.LENGTH_SHORT) {
431             locIdName = langData.get("Languages%short", localeId);
432             if (locIdName != null && !locIdName.equals(localeId)) {
433                 return locIdName;
434             }
435         }
436         locIdName = langData.get("Languages", localeId);
437         if ((locIdName == null || locIdName.equals(localeId)) && localeId.indexOf('_') < 0) {
438             // Canonicalize lang and try again, ICU-20870
439             // (only for language codes without script or region)
440             ULocale canonLocale = ULocale.createCanonical(localeId);
441             String canonLocId = canonLocale.getName();
442             if (nameLength == DisplayContext.LENGTH_SHORT) {
443                 locIdName = langData.get("Languages%short", canonLocId);
444                 if (locIdName != null && !locIdName.equals(canonLocId)) {
445                     return locIdName;
446                 }
447             }
448             locIdName = langData.get("Languages", canonLocId);
449         }
450         return locIdName;
451     }
452 
453     @Override
languageDisplayName(String lang)454     public String languageDisplayName(String lang) {
455         // Special case to eliminate non-languages, which pollute our data.
456         if (lang.equals("root") || lang.indexOf('_') != -1) {
457             return substituteHandling == DisplayContext.SUBSTITUTE ? lang : null;
458         }
459         String langName;
460         if (nameLength == DisplayContext.LENGTH_SHORT) {
461             langName = langData.get("Languages%short", lang);
462             if (langName != null && !langName.equals(lang)) {
463                 return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName);
464             }
465         }
466         langName = langData.get("Languages", lang);
467         if (langName == null || langName.equals(lang)) {
468             // Canonicalize lang and try again, ICU-20870
469             ULocale canonLocale = ULocale.createCanonical(lang);
470             String canonLocId = canonLocale.getName();
471             if (nameLength == DisplayContext.LENGTH_SHORT) {
472                 langName = langData.get("Languages%short", canonLocId);
473                 if (langName != null && !langName.equals(canonLocId)) {
474                     return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName);
475                 }
476             }
477             langName = langData.get("Languages", canonLocId);
478         }
479         return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName);
480     }
481 
482     @Override
scriptDisplayName(String script)483     public String scriptDisplayName(String script) {
484         String str = langData.get("Scripts%stand-alone", script);
485         if (str == null || str.equals(script)) {
486             if (nameLength == DisplayContext.LENGTH_SHORT) {
487                 str = langData.get("Scripts%short", script);
488                 if (str != null && !str.equals(script)) {
489                     return adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, str);
490                 }
491             }
492             str = langData.get("Scripts", script);
493         }
494         return adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, str);
495     }
496 
scriptDisplayNameInContext(String script, boolean skipAdjust)497     private String scriptDisplayNameInContext(String script, boolean skipAdjust) {
498         if (nameLength == DisplayContext.LENGTH_SHORT) {
499             String scriptName = langData.get("Scripts%short", script);
500             if (scriptName != null && !scriptName.equals(script)) {
501                 return skipAdjust? scriptName: adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, scriptName);
502             }
503         }
504         String scriptName = langData.get("Scripts", script);
505         return skipAdjust? scriptName: adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, scriptName);
506     }
507 
508     @Override
scriptDisplayNameInContext(String script)509     public String scriptDisplayNameInContext(String script) {
510         return scriptDisplayNameInContext(script, false);
511     }
512 
513     @Override
scriptDisplayName(int scriptCode)514     public String scriptDisplayName(int scriptCode) {
515         return scriptDisplayName(UScript.getShortName(scriptCode));
516     }
517 
regionDisplayName(String region, boolean skipAdjust)518     private String regionDisplayName(String region, boolean skipAdjust) {
519         if (nameLength == DisplayContext.LENGTH_SHORT) {
520             String regionName = regionData.get("Countries%short", region);
521             if (regionName != null && !regionName.equals(region)) {
522                 return skipAdjust? regionName: adjustForUsageAndContext(CapitalizationContextUsage.TERRITORY, regionName);
523             }
524         }
525         String regionName = regionData.get("Countries", region);
526         return skipAdjust? regionName: adjustForUsageAndContext(CapitalizationContextUsage.TERRITORY, regionName);
527     }
528 
529     @Override
regionDisplayName(String region)530     public String regionDisplayName(String region) {
531         return regionDisplayName(region, false);
532     }
533 
variantDisplayName(String variant, boolean skipAdjust)534     private String variantDisplayName(String variant, boolean skipAdjust) {
535         // don't have a resource for short variant names
536         String variantName = langData.get("Variants", variant);
537         return skipAdjust? variantName: adjustForUsageAndContext(CapitalizationContextUsage.VARIANT, variantName);
538     }
539 
540     @Override
variantDisplayName(String variant)541     public String variantDisplayName(String variant) {
542         return variantDisplayName(variant, false);
543     }
544 
keyDisplayName(String key, boolean skipAdjust)545     private String keyDisplayName(String key, boolean skipAdjust) {
546         // don't have a resource for short key names
547         String keyName = langData.get("Keys", key);
548         return skipAdjust? keyName: adjustForUsageAndContext(CapitalizationContextUsage.KEY, keyName);
549     }
550 
551     @Override
keyDisplayName(String key)552     public String keyDisplayName(String key) {
553         return keyDisplayName(key, false);
554     }
555 
keyValueDisplayName(String key, String value, boolean skipAdjust)556     private String keyValueDisplayName(String key, String value, boolean skipAdjust) {
557         String keyValueName = null;
558 
559         if (key.equals("currency")) {
560             keyValueName = currencyDisplayInfo.getName(AsciiUtil.toUpperString(value));
561             if (keyValueName == null) {
562                 keyValueName = value;
563             }
564         } else {
565             if (nameLength == DisplayContext.LENGTH_SHORT) {
566                 String tmp = langData.get("Types%short", key, value);
567                 if (tmp != null && !tmp.equals(value)) {
568                     keyValueName = tmp;
569                 }
570             }
571             if (keyValueName == null) {
572                 keyValueName = langData.get("Types", key, value);
573             }
574         }
575 
576         return skipAdjust? keyValueName: adjustForUsageAndContext(CapitalizationContextUsage.KEYVALUE, keyValueName);
577     }
578 
579     @Override
keyValueDisplayName(String key, String value)580     public String keyValueDisplayName(String key, String value) {
581         return keyValueDisplayName(key, value, false);
582     }
583 
584     @Override
getUiListCompareWholeItems(Set<ULocale> localeSet, Comparator<UiListItem> comparator)585     public List<UiListItem> getUiListCompareWholeItems(Set<ULocale> localeSet, Comparator<UiListItem> comparator) {
586         DisplayContext capContext = getContext(Type.CAPITALIZATION);
587 
588         List<UiListItem> result = new ArrayList<UiListItem>();
589         Map<ULocale,Set<ULocale>> baseToLocales = new HashMap<ULocale,Set<ULocale>>();
590         ULocale.Builder builder = new ULocale.Builder();
591         for (ULocale locOriginal : localeSet) {
592             builder.setLocale(locOriginal); // verify well-formed. We do this here so that we consistently throw exception
593             ULocale loc = ULocale.addLikelySubtags(locOriginal);
594             ULocale base = new ULocale(loc.getLanguage());
595             Set<ULocale> locales = baseToLocales.get(base);
596             if (locales == null) {
597                 baseToLocales.put(base, locales = new HashSet<ULocale>());
598             }
599             locales.add(loc);
600         }
601         for (Entry<ULocale, Set<ULocale>> entry : baseToLocales.entrySet()) {
602             ULocale base = entry.getKey();
603             Set<ULocale> values = entry.getValue();
604             if (values.size() == 1) {
605                 ULocale locale = values.iterator().next();
606                 result.add(newRow(ULocale.minimizeSubtags(locale, ULocale.Minimize.FAVOR_SCRIPT), capContext));
607             } else {
608                 Set<String> scripts = new HashSet<String>();
609                 Set<String> regions = new HashSet<String>();
610                 // need the follow two steps to make sure that unusual scripts or regions are displayed
611                 ULocale maxBase = ULocale.addLikelySubtags(base);
612                 scripts.add(maxBase.getScript());
613                 regions.add(maxBase.getCountry());
614                 for (ULocale locale : values) {
615                     scripts.add(locale.getScript());
616                     regions.add(locale.getCountry());
617                 }
618                 boolean hasScripts = scripts.size() > 1;
619                 boolean hasRegions = regions.size() > 1;
620                 for (ULocale locale : values) {
621                     ULocale.Builder modified = builder.setLocale(locale);
622                     if (!hasScripts) {
623                         modified.setScript("");
624                     }
625                     if (!hasRegions) {
626                         modified.setRegion("");
627                     }
628                     result.add(newRow(modified.build(), capContext));
629                 }
630             }
631         }
632         Collections.sort(result, comparator);
633         return result;
634     }
635 
newRow(ULocale modified, DisplayContext capContext)636     private UiListItem newRow(ULocale modified, DisplayContext capContext) {
637         ULocale minimized = ULocale.minimizeSubtags(modified, ULocale.Minimize.FAVOR_SCRIPT);
638         String tempName = modified.getDisplayName(locale);
639         boolean titlecase = capContext == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU;
640         String nameInDisplayLocale =
641                 titlecase ? toTitleWholeStringNoLowercase(locale, tempName) : tempName;
642         tempName = modified.getDisplayName(modified);
643         String nameInSelf = capContext ==
644                 DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU ?
645                         toTitleWholeStringNoLowercase(modified, tempName) : tempName;
646         return new UiListItem(minimized, modified, nameInDisplayLocale, nameInSelf);
647     }
648 
649     public static class DataTable {
650         final boolean nullIfNotFound;
651 
DataTable(boolean nullIfNotFound)652         DataTable(boolean nullIfNotFound) {
653             this.nullIfNotFound = nullIfNotFound;
654         }
655 
getLocale()656         ULocale getLocale() {
657             return ULocale.ROOT;
658         }
659 
get(String tableName, String code)660         String get(String tableName, String code) {
661             return get(tableName, null, code);
662         }
663 
get(String tableName, String subTableName, String code)664         String get(String tableName, String subTableName, String code) {
665             return nullIfNotFound ? null : code;
666         }
667     }
668 
669     static class ICUDataTable extends DataTable {
670         private final ICUResourceBundle bundle;
671 
ICUDataTable(String path, ULocale locale, boolean nullIfNotFound)672         public ICUDataTable(String path, ULocale locale, boolean nullIfNotFound) {
673             super(nullIfNotFound);
674             this.bundle = (ICUResourceBundle) UResourceBundle.getBundleInstance(
675                     path, locale.getBaseName());
676         }
677 
678         @Override
getLocale()679         public ULocale getLocale() {
680             return bundle.getULocale();
681         }
682 
683         @Override
get(String tableName, String subTableName, String code)684         public String get(String tableName, String subTableName, String code) {
685             return ICUResourceTableAccess.getTableString(bundle, tableName, subTableName,
686                     code, nullIfNotFound ? null : code);
687         }
688     }
689 
690     static abstract class DataTables {
get(ULocale locale, boolean nullIfNotFound)691         public abstract DataTable get(ULocale locale, boolean nullIfNotFound);
load(String className)692         public static DataTables load(String className) {
693             try {
694                 return (DataTables) Class.forName(className).newInstance();
695             } catch (Throwable t) {
696                 return new DataTables() {
697                     @Override
698                     public DataTable get(ULocale locale, boolean nullIfNotFound) {
699                         return new DataTable(nullIfNotFound);
700                     }
701                 };
702             }
703         }
704     }
705 
706     static abstract class ICUDataTables extends DataTables {
707         private final String path;
708 
ICUDataTables(String path)709         protected ICUDataTables(String path) {
710             this.path = path;
711         }
712 
713         @Override
get(ULocale locale, boolean nullIfNotFound)714         public DataTable get(ULocale locale, boolean nullIfNotFound) {
715             return new ICUDataTable(path, locale, nullIfNotFound);
716         }
717     }
718 
719     static class LangDataTables {
720         static final DataTables impl = DataTables.load("com.ibm.icu.impl.ICULangDataTables");
721     }
722 
723     static class RegionDataTables {
724         static final DataTables impl = DataTables.load("com.ibm.icu.impl.ICURegionDataTables");
725     }
726 
727     public static enum DataTableType {
728         LANG, REGION;
729     }
730 
haveData(DataTableType type)731     public static boolean haveData(DataTableType type) {
732         switch (type) {
733         case LANG: return LangDataTables.impl instanceof ICUDataTables;
734         case REGION: return RegionDataTables.impl instanceof ICUDataTables;
735         default:
736             throw new IllegalArgumentException("unknown type: " + type);
737         }
738     }
739 
appendWithSep(String s, StringBuilder b)740     private StringBuilder appendWithSep(String s, StringBuilder b) {
741         if (b.length() == 0) {
742             b.append(s);
743         } else {
744             SimpleFormatterImpl.formatAndReplace(separatorFormat, b, null, b, s);
745         }
746         return b;
747     }
748 
749     private static class Cache {
750         private ULocale locale;
751         private DialectHandling dialectHandling;
752         private DisplayContext capitalization;
753         private DisplayContext nameLength;
754         private DisplayContext substituteHandling;
755         private LocaleDisplayNames cache;
get(ULocale locale, DialectHandling dialectHandling)756         public LocaleDisplayNames get(ULocale locale, DialectHandling dialectHandling) {
757             if (!(dialectHandling == this.dialectHandling && DisplayContext.CAPITALIZATION_NONE == this.capitalization &&
758                     DisplayContext.LENGTH_FULL == this.nameLength && DisplayContext.SUBSTITUTE == this.substituteHandling &&
759                     locale.equals(this.locale))) {
760                 this.locale = locale;
761                 this.dialectHandling = dialectHandling;
762                 this.capitalization = DisplayContext.CAPITALIZATION_NONE;
763                 this.nameLength = DisplayContext.LENGTH_FULL;
764                 this.substituteHandling = DisplayContext.SUBSTITUTE;
765                 this.cache = new LocaleDisplayNamesImpl(locale, dialectHandling);
766             }
767             return cache;
768         }
get(ULocale locale, DisplayContext... contexts)769         public LocaleDisplayNames get(ULocale locale, DisplayContext... contexts) {
770             DialectHandling dialectHandlingIn = DialectHandling.STANDARD_NAMES;
771             DisplayContext capitalizationIn = DisplayContext.CAPITALIZATION_NONE;
772             DisplayContext nameLengthIn = DisplayContext.LENGTH_FULL;
773             DisplayContext substituteHandling = DisplayContext.SUBSTITUTE;
774             for (DisplayContext contextItem : contexts) {
775                 switch (contextItem.type()) {
776                 case DIALECT_HANDLING:
777                     dialectHandlingIn = (contextItem.value()==DisplayContext.STANDARD_NAMES.value())?
778                             DialectHandling.STANDARD_NAMES: DialectHandling.DIALECT_NAMES;
779                     break;
780                 case CAPITALIZATION:
781                     capitalizationIn = contextItem;
782                     break;
783                 case DISPLAY_LENGTH:
784                     nameLengthIn = contextItem;
785                     break;
786                 case SUBSTITUTE_HANDLING:
787                     substituteHandling = contextItem;
788                     break;
789                 default:
790                     break;
791                 }
792             }
793             if (!(dialectHandlingIn == this.dialectHandling && capitalizationIn == this.capitalization &&
794                     nameLengthIn == this.nameLength && substituteHandling == this.substituteHandling &&
795                     locale.equals(this.locale))) {
796                 this.locale = locale;
797                 this.dialectHandling = dialectHandlingIn;
798                 this.capitalization = capitalizationIn;
799                 this.nameLength = nameLengthIn;
800                 this.substituteHandling = substituteHandling;
801                 this.cache = new LocaleDisplayNamesImpl(locale, contexts);
802             }
803             return cache;
804         }
805     }
806 }
807