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         if (nameLength == DisplayContext.LENGTH_SHORT) {
430             String locIdName = langData.get("Languages%short", localeId);
431             if (locIdName != null && !locIdName.equals(localeId)) {
432                 return locIdName;
433             }
434         }
435         return langData.get("Languages", localeId);
436     }
437 
438     @Override
languageDisplayName(String lang)439     public String languageDisplayName(String lang) {
440         // Special case to eliminate non-languages, which pollute our data.
441         if (lang.equals("root") || lang.indexOf('_') != -1) {
442             return substituteHandling == DisplayContext.SUBSTITUTE ? lang : null;
443         }
444         if (nameLength == DisplayContext.LENGTH_SHORT) {
445             String langName = langData.get("Languages%short", lang);
446             if (langName != null && !langName.equals(lang)) {
447                 return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName);
448             }
449         }
450         return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langData.get("Languages", lang));
451     }
452 
453     @Override
scriptDisplayName(String script)454     public String scriptDisplayName(String script) {
455         String str = langData.get("Scripts%stand-alone", script);
456         if (str == null || str.equals(script)) {
457             if (nameLength == DisplayContext.LENGTH_SHORT) {
458                 str = langData.get("Scripts%short", script);
459                 if (str != null && !str.equals(script)) {
460                     return adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, str);
461                 }
462             }
463             str = langData.get("Scripts", script);
464         }
465         return adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, str);
466     }
467 
scriptDisplayNameInContext(String script, boolean skipAdjust)468     private String scriptDisplayNameInContext(String script, boolean skipAdjust) {
469         if (nameLength == DisplayContext.LENGTH_SHORT) {
470             String scriptName = langData.get("Scripts%short", script);
471             if (scriptName != null && !scriptName.equals(script)) {
472                 return skipAdjust? scriptName: adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, scriptName);
473             }
474         }
475         String scriptName = langData.get("Scripts", script);
476         return skipAdjust? scriptName: adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, scriptName);
477     }
478 
479     @Override
scriptDisplayNameInContext(String script)480     public String scriptDisplayNameInContext(String script) {
481         return scriptDisplayNameInContext(script, false);
482     }
483 
484     @Override
scriptDisplayName(int scriptCode)485     public String scriptDisplayName(int scriptCode) {
486         return scriptDisplayName(UScript.getShortName(scriptCode));
487     }
488 
regionDisplayName(String region, boolean skipAdjust)489     private String regionDisplayName(String region, boolean skipAdjust) {
490         if (nameLength == DisplayContext.LENGTH_SHORT) {
491             String regionName = regionData.get("Countries%short", region);
492             if (regionName != null && !regionName.equals(region)) {
493                 return skipAdjust? regionName: adjustForUsageAndContext(CapitalizationContextUsage.TERRITORY, regionName);
494             }
495         }
496         String regionName = regionData.get("Countries", region);
497         return skipAdjust? regionName: adjustForUsageAndContext(CapitalizationContextUsage.TERRITORY, regionName);
498     }
499 
500     @Override
regionDisplayName(String region)501     public String regionDisplayName(String region) {
502         return regionDisplayName(region, false);
503     }
504 
variantDisplayName(String variant, boolean skipAdjust)505     private String variantDisplayName(String variant, boolean skipAdjust) {
506         // don't have a resource for short variant names
507         String variantName = langData.get("Variants", variant);
508         return skipAdjust? variantName: adjustForUsageAndContext(CapitalizationContextUsage.VARIANT, variantName);
509     }
510 
511     @Override
variantDisplayName(String variant)512     public String variantDisplayName(String variant) {
513         return variantDisplayName(variant, false);
514     }
515 
keyDisplayName(String key, boolean skipAdjust)516     private String keyDisplayName(String key, boolean skipAdjust) {
517         // don't have a resource for short key names
518         String keyName = langData.get("Keys", key);
519         return skipAdjust? keyName: adjustForUsageAndContext(CapitalizationContextUsage.KEY, keyName);
520     }
521 
522     @Override
keyDisplayName(String key)523     public String keyDisplayName(String key) {
524         return keyDisplayName(key, false);
525     }
526 
keyValueDisplayName(String key, String value, boolean skipAdjust)527     private String keyValueDisplayName(String key, String value, boolean skipAdjust) {
528         String keyValueName = null;
529 
530         if (key.equals("currency")) {
531             keyValueName = currencyDisplayInfo.getName(AsciiUtil.toUpperString(value));
532             if (keyValueName == null) {
533                 keyValueName = value;
534             }
535         } else {
536             if (nameLength == DisplayContext.LENGTH_SHORT) {
537                 String tmp = langData.get("Types%short", key, value);
538                 if (tmp != null && !tmp.equals(value)) {
539                     keyValueName = tmp;
540                 }
541             }
542             if (keyValueName == null) {
543                 keyValueName = langData.get("Types", key, value);
544             }
545         }
546 
547         return skipAdjust? keyValueName: adjustForUsageAndContext(CapitalizationContextUsage.KEYVALUE, keyValueName);
548     }
549 
550     @Override
keyValueDisplayName(String key, String value)551     public String keyValueDisplayName(String key, String value) {
552         return keyValueDisplayName(key, value, false);
553     }
554 
555     @Override
getUiListCompareWholeItems(Set<ULocale> localeSet, Comparator<UiListItem> comparator)556     public List<UiListItem> getUiListCompareWholeItems(Set<ULocale> localeSet, Comparator<UiListItem> comparator) {
557         DisplayContext capContext = getContext(Type.CAPITALIZATION);
558 
559         List<UiListItem> result = new ArrayList<UiListItem>();
560         Map<ULocale,Set<ULocale>> baseToLocales = new HashMap<ULocale,Set<ULocale>>();
561         ULocale.Builder builder = new ULocale.Builder();
562         for (ULocale locOriginal : localeSet) {
563             builder.setLocale(locOriginal); // verify well-formed. We do this here so that we consistently throw exception
564             ULocale loc = ULocale.addLikelySubtags(locOriginal);
565             ULocale base = new ULocale(loc.getLanguage());
566             Set<ULocale> locales = baseToLocales.get(base);
567             if (locales == null) {
568                 baseToLocales.put(base, locales = new HashSet<ULocale>());
569             }
570             locales.add(loc);
571         }
572         for (Entry<ULocale, Set<ULocale>> entry : baseToLocales.entrySet()) {
573             ULocale base = entry.getKey();
574             Set<ULocale> values = entry.getValue();
575             if (values.size() == 1) {
576                 ULocale locale = values.iterator().next();
577                 result.add(newRow(ULocale.minimizeSubtags(locale, ULocale.Minimize.FAVOR_SCRIPT), capContext));
578             } else {
579                 Set<String> scripts = new HashSet<String>();
580                 Set<String> regions = new HashSet<String>();
581                 // need the follow two steps to make sure that unusual scripts or regions are displayed
582                 ULocale maxBase = ULocale.addLikelySubtags(base);
583                 scripts.add(maxBase.getScript());
584                 regions.add(maxBase.getCountry());
585                 for (ULocale locale : values) {
586                     scripts.add(locale.getScript());
587                     regions.add(locale.getCountry());
588                 }
589                 boolean hasScripts = scripts.size() > 1;
590                 boolean hasRegions = regions.size() > 1;
591                 for (ULocale locale : values) {
592                     ULocale.Builder modified = builder.setLocale(locale);
593                     if (!hasScripts) {
594                         modified.setScript("");
595                     }
596                     if (!hasRegions) {
597                         modified.setRegion("");
598                     }
599                     result.add(newRow(modified.build(), capContext));
600                 }
601             }
602         }
603         Collections.sort(result, comparator);
604         return result;
605     }
606 
newRow(ULocale modified, DisplayContext capContext)607     private UiListItem newRow(ULocale modified, DisplayContext capContext) {
608         ULocale minimized = ULocale.minimizeSubtags(modified, ULocale.Minimize.FAVOR_SCRIPT);
609         String tempName = modified.getDisplayName(locale);
610         boolean titlecase = capContext == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU;
611         String nameInDisplayLocale =
612                 titlecase ? toTitleWholeStringNoLowercase(locale, tempName) : tempName;
613         tempName = modified.getDisplayName(modified);
614         String nameInSelf = capContext ==
615                 DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU ?
616                         toTitleWholeStringNoLowercase(modified, tempName) : tempName;
617         return new UiListItem(minimized, modified, nameInDisplayLocale, nameInSelf);
618     }
619 
620     public static class DataTable {
621         final boolean nullIfNotFound;
622 
DataTable(boolean nullIfNotFound)623         DataTable(boolean nullIfNotFound) {
624             this.nullIfNotFound = nullIfNotFound;
625         }
626 
getLocale()627         ULocale getLocale() {
628             return ULocale.ROOT;
629         }
630 
get(String tableName, String code)631         String get(String tableName, String code) {
632             return get(tableName, null, code);
633         }
634 
get(String tableName, String subTableName, String code)635         String get(String tableName, String subTableName, String code) {
636             return nullIfNotFound ? null : code;
637         }
638     }
639 
640     static class ICUDataTable extends DataTable {
641         private final ICUResourceBundle bundle;
642 
ICUDataTable(String path, ULocale locale, boolean nullIfNotFound)643         public ICUDataTable(String path, ULocale locale, boolean nullIfNotFound) {
644             super(nullIfNotFound);
645             this.bundle = (ICUResourceBundle) UResourceBundle.getBundleInstance(
646                     path, locale.getBaseName());
647         }
648 
649         @Override
getLocale()650         public ULocale getLocale() {
651             return bundle.getULocale();
652         }
653 
654         @Override
get(String tableName, String subTableName, String code)655         public String get(String tableName, String subTableName, String code) {
656             return ICUResourceTableAccess.getTableString(bundle, tableName, subTableName,
657                     code, nullIfNotFound ? null : code);
658         }
659     }
660 
661     static abstract class DataTables {
get(ULocale locale, boolean nullIfNotFound)662         public abstract DataTable get(ULocale locale, boolean nullIfNotFound);
load(String className)663         public static DataTables load(String className) {
664             try {
665                 return (DataTables) Class.forName(className).newInstance();
666             } catch (Throwable t) {
667                 return new DataTables() {
668                     @Override
669                     public DataTable get(ULocale locale, boolean nullIfNotFound) {
670                         return new DataTable(nullIfNotFound);
671                     }
672                 };
673             }
674         }
675     }
676 
677     static abstract class ICUDataTables extends DataTables {
678         private final String path;
679 
ICUDataTables(String path)680         protected ICUDataTables(String path) {
681             this.path = path;
682         }
683 
684         @Override
get(ULocale locale, boolean nullIfNotFound)685         public DataTable get(ULocale locale, boolean nullIfNotFound) {
686             return new ICUDataTable(path, locale, nullIfNotFound);
687         }
688     }
689 
690     static class LangDataTables {
691         static final DataTables impl = DataTables.load("com.ibm.icu.impl.ICULangDataTables");
692     }
693 
694     static class RegionDataTables {
695         static final DataTables impl = DataTables.load("com.ibm.icu.impl.ICURegionDataTables");
696     }
697 
698     public static enum DataTableType {
699         LANG, REGION;
700     }
701 
haveData(DataTableType type)702     public static boolean haveData(DataTableType type) {
703         switch (type) {
704         case LANG: return LangDataTables.impl instanceof ICUDataTables;
705         case REGION: return RegionDataTables.impl instanceof ICUDataTables;
706         default:
707             throw new IllegalArgumentException("unknown type: " + type);
708         }
709     }
710 
appendWithSep(String s, StringBuilder b)711     private StringBuilder appendWithSep(String s, StringBuilder b) {
712         if (b.length() == 0) {
713             b.append(s);
714         } else {
715             SimpleFormatterImpl.formatAndReplace(separatorFormat, b, null, b, s);
716         }
717         return b;
718     }
719 
720     private static class Cache {
721         private ULocale locale;
722         private DialectHandling dialectHandling;
723         private DisplayContext capitalization;
724         private DisplayContext nameLength;
725         private DisplayContext substituteHandling;
726         private LocaleDisplayNames cache;
get(ULocale locale, DialectHandling dialectHandling)727         public LocaleDisplayNames get(ULocale locale, DialectHandling dialectHandling) {
728             if (!(dialectHandling == this.dialectHandling && DisplayContext.CAPITALIZATION_NONE == this.capitalization &&
729                     DisplayContext.LENGTH_FULL == this.nameLength && DisplayContext.SUBSTITUTE == this.substituteHandling &&
730                     locale.equals(this.locale))) {
731                 this.locale = locale;
732                 this.dialectHandling = dialectHandling;
733                 this.capitalization = DisplayContext.CAPITALIZATION_NONE;
734                 this.nameLength = DisplayContext.LENGTH_FULL;
735                 this.substituteHandling = DisplayContext.SUBSTITUTE;
736                 this.cache = new LocaleDisplayNamesImpl(locale, dialectHandling);
737             }
738             return cache;
739         }
get(ULocale locale, DisplayContext... contexts)740         public LocaleDisplayNames get(ULocale locale, DisplayContext... contexts) {
741             DialectHandling dialectHandlingIn = DialectHandling.STANDARD_NAMES;
742             DisplayContext capitalizationIn = DisplayContext.CAPITALIZATION_NONE;
743             DisplayContext nameLengthIn = DisplayContext.LENGTH_FULL;
744             DisplayContext substituteHandling = DisplayContext.SUBSTITUTE;
745             for (DisplayContext contextItem : contexts) {
746                 switch (contextItem.type()) {
747                 case DIALECT_HANDLING:
748                     dialectHandlingIn = (contextItem.value()==DisplayContext.STANDARD_NAMES.value())?
749                             DialectHandling.STANDARD_NAMES: DialectHandling.DIALECT_NAMES;
750                     break;
751                 case CAPITALIZATION:
752                     capitalizationIn = contextItem;
753                     break;
754                 case DISPLAY_LENGTH:
755                     nameLengthIn = contextItem;
756                     break;
757                 case SUBSTITUTE_HANDLING:
758                     substituteHandling = contextItem;
759                     break;
760                 default:
761                     break;
762                 }
763             }
764             if (!(dialectHandlingIn == this.dialectHandling && capitalizationIn == this.capitalization &&
765                     nameLengthIn == this.nameLength && substituteHandling == this.substituteHandling &&
766                     locale.equals(this.locale))) {
767                 this.locale = locale;
768                 this.dialectHandling = dialectHandlingIn;
769                 this.capitalization = capitalizationIn;
770                 this.nameLength = nameLengthIn;
771                 this.substituteHandling = substituteHandling;
772                 this.cache = new LocaleDisplayNamesImpl(locale, contexts);
773             }
774             return cache;
775         }
776     }
777 }
778