1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
3 /*
4  *******************************************************************************
5  * Copyright (C) 2008-2016, International Business Machines Corporation and
6  * others. All Rights Reserved.
7  *******************************************************************************
8  */
9 package com.ibm.icu.impl;
10 
11 import java.text.ParseException;
12 import java.util.Collections;
13 import java.util.HashMap;
14 import java.util.Iterator;
15 import java.util.Map;
16 import java.util.MissingResourceException;
17 import java.util.Set;
18 import java.util.TreeMap;
19 
20 import com.ibm.icu.text.PluralRanges;
21 import com.ibm.icu.text.PluralRules;
22 import com.ibm.icu.text.PluralRules.PluralType;
23 import com.ibm.icu.util.ULocale;
24 import com.ibm.icu.util.UResourceBundle;
25 
26 /**
27  * Loader for plural rules data.
28  */
29 public class PluralRulesLoader extends PluralRules.Factory {
30     private final Map<String, PluralRules> rulesIdToRules;
31     // lazy init, use getLocaleIdToRulesIdMap to access
32     private Map<String, String> localeIdToCardinalRulesId;
33     private Map<String, String> localeIdToOrdinalRulesId;
34     private Map<String, ULocale> rulesIdToEquivalentULocale;
35     private static Map<String, PluralRanges> localeIdToPluralRanges;
36 
37 
38     /**
39      * Access through singleton.
40      */
PluralRulesLoader()41     private PluralRulesLoader() {
42         rulesIdToRules = new HashMap<String, PluralRules>();
43     }
44 
45     /**
46      * Returns the locales for which we have plurals data. Utility for testing.
47      */
getAvailableULocales()48     public ULocale[] getAvailableULocales() {
49         Set<String> keys = getLocaleIdToRulesIdMap(PluralType.CARDINAL).keySet();
50         ULocale[] locales = new ULocale[keys.size()];
51         int n = 0;
52         for (Iterator<String> iter = keys.iterator(); iter.hasNext();) {
53             locales[n++] = ULocale.createCanonical(iter.next());
54         }
55         return locales;
56     }
57 
58     /**
59      * Returns the functionally equivalent locale.
60      */
getFunctionalEquivalent(ULocale locale, boolean[] isAvailable)61     public ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) {
62         if (isAvailable != null && isAvailable.length > 0) {
63             String localeId = ULocale.canonicalize(locale.getBaseName());
64             Map<String, String> idMap = getLocaleIdToRulesIdMap(PluralType.CARDINAL);
65             isAvailable[0] = idMap.containsKey(localeId);
66         }
67 
68         String rulesId = getRulesIdForLocale(locale, PluralType.CARDINAL);
69         if (rulesId == null || rulesId.trim().length() == 0) {
70             return ULocale.ROOT; // ultimate fallback
71         }
72 
73         ULocale result = getRulesIdToEquivalentULocaleMap().get(
74                 rulesId);
75         if (result == null) {
76             return ULocale.ROOT; // ultimate fallback
77         }
78 
79         return result;
80     }
81 
82     /**
83      * Returns the lazily-constructed map.
84      */
getLocaleIdToRulesIdMap(PluralType type)85     private Map<String, String> getLocaleIdToRulesIdMap(PluralType type) {
86         checkBuildRulesIdMaps();
87         return (type == PluralType.CARDINAL) ? localeIdToCardinalRulesId : localeIdToOrdinalRulesId;
88     }
89 
90     /**
91      * Returns the lazily-constructed map.
92      */
getRulesIdToEquivalentULocaleMap()93     private Map<String, ULocale> getRulesIdToEquivalentULocaleMap() {
94         checkBuildRulesIdMaps();
95         return rulesIdToEquivalentULocale;
96     }
97 
98     /**
99      * Lazily constructs the localeIdToRulesId and rulesIdToEquivalentULocale
100      * maps if necessary. These exactly reflect the contents of the locales
101      * resource in plurals.res.
102      */
checkBuildRulesIdMaps()103     private void checkBuildRulesIdMaps() {
104         boolean haveMap;
105         synchronized (this) {
106             haveMap = localeIdToCardinalRulesId != null;
107         }
108         if (!haveMap) {
109             Map<String, String> tempLocaleIdToCardinalRulesId;
110             Map<String, String> tempLocaleIdToOrdinalRulesId;
111             Map<String, ULocale> tempRulesIdToEquivalentULocale;
112             try {
113                 UResourceBundle pluralb = getPluralBundle();
114                 // Read cardinal-number rules.
115                 UResourceBundle localeb = pluralb.get("locales");
116 
117                 // sort for convenience of getAvailableULocales
118                 tempLocaleIdToCardinalRulesId = new TreeMap<String, String>();
119                 // not visible
120                 tempRulesIdToEquivalentULocale = new HashMap<String, ULocale>();
121 
122                 for (int i = 0; i < localeb.getSize(); ++i) {
123                     UResourceBundle b = localeb.get(i);
124                     String id = b.getKey();
125                     String value = b.getString().intern();
126                     tempLocaleIdToCardinalRulesId.put(id, value);
127 
128                     if (!tempRulesIdToEquivalentULocale.containsKey(value)) {
129                         tempRulesIdToEquivalentULocale.put(value, new ULocale(id));
130                     }
131                 }
132 
133                 // Read ordinal-number rules.
134                 localeb = pluralb.get("locales_ordinals");
135                 tempLocaleIdToOrdinalRulesId = new TreeMap<String, String>();
136                 for (int i = 0; i < localeb.getSize(); ++i) {
137                     UResourceBundle b = localeb.get(i);
138                     String id = b.getKey();
139                     String value = b.getString().intern();
140                     tempLocaleIdToOrdinalRulesId.put(id, value);
141                 }
142             } catch (MissingResourceException e) {
143                 // dummy so we don't try again
144                 tempLocaleIdToCardinalRulesId = Collections.emptyMap();
145                 tempLocaleIdToOrdinalRulesId = Collections.emptyMap();
146                 tempRulesIdToEquivalentULocale = Collections.emptyMap();
147             }
148 
149             synchronized(this) {
150                 if (localeIdToCardinalRulesId == null) {
151                     localeIdToCardinalRulesId = tempLocaleIdToCardinalRulesId;
152                     localeIdToOrdinalRulesId = tempLocaleIdToOrdinalRulesId;
153                     rulesIdToEquivalentULocale = tempRulesIdToEquivalentULocale;
154                 }
155             }
156         }
157     }
158 
159     /**
160      * Gets the rulesId from the locale,with locale fallback. If there is no
161      * rulesId, return null. The rulesId might be the empty string if the rule
162      * is the default rule.
163      */
getRulesIdForLocale(ULocale locale, PluralType type)164     public String getRulesIdForLocale(ULocale locale, PluralType type) {
165         Map<String, String> idMap = getLocaleIdToRulesIdMap(type);
166         String localeId = ULocale.canonicalize(locale.getBaseName());
167         String rulesId = null;
168         while (null == (rulesId = idMap.get(localeId))) {
169             int ix = localeId.lastIndexOf("_");
170             if (ix == -1) {
171                 break;
172             }
173             localeId = localeId.substring(0, ix);
174         }
175         return rulesId;
176     }
177 
178     /**
179      * Gets the rule from the rulesId. If there is no rule for this rulesId,
180      * return null.
181      */
getRulesForRulesId(String rulesId)182     public PluralRules getRulesForRulesId(String rulesId) {
183         // synchronize on the map.  release the lock temporarily while we build the rules.
184         PluralRules rules = null;
185         boolean hasRules;  // Separate boolean because stored rules can be null.
186         synchronized (rulesIdToRules) {
187             hasRules = rulesIdToRules.containsKey(rulesId);
188             if (hasRules) {
189                 rules = rulesIdToRules.get(rulesId);  // can be null
190             }
191         }
192         if (!hasRules) {
193             try {
194                 UResourceBundle pluralb = getPluralBundle();
195                 UResourceBundle rulesb = pluralb.get("rules");
196                 UResourceBundle setb = rulesb.get(rulesId);
197 
198                 StringBuilder sb = new StringBuilder();
199                 for (int i = 0; i < setb.getSize(); ++i) {
200                     UResourceBundle b = setb.get(i);
201                     if (i > 0) {
202                         sb.append("; ");
203                     }
204                     sb.append(b.getKey());
205                     sb.append(": ");
206                     sb.append(b.getString());
207                 }
208                 rules = PluralRules.parseDescription(sb.toString());
209             } catch (ParseException e) {
210             } catch (MissingResourceException e) {
211             }
212             synchronized (rulesIdToRules) {
213                 if (rulesIdToRules.containsKey(rulesId)) {
214                     rules = rulesIdToRules.get(rulesId);
215                 } else {
216                     rulesIdToRules.put(rulesId, rules);  // can be null
217                 }
218             }
219         }
220         return rules;
221     }
222 
223     /**
224      * Return the plurals resource. Note MissingResourceException is unchecked,
225      * listed here for clarity. Callers should handle this exception.
226      */
getPluralBundle()227     public UResourceBundle getPluralBundle() throws MissingResourceException {
228         return ICUResourceBundle.getBundleInstance(
229                 ICUData.ICU_BASE_NAME, "plurals",
230                 ICUResourceBundle.ICU_DATA_CLASS_LOADER, true);
231     }
232 
233     /**
234      * Returns the plural rules for the the locale. If we don't have data,
235      * com.ibm.icu.text.PluralRules.DEFAULT is returned.
236      */
forLocale(ULocale locale, PluralRules.PluralType type)237     public PluralRules forLocale(ULocale locale, PluralRules.PluralType type) {
238         String rulesId = getRulesIdForLocale(locale, type);
239         if (rulesId == null || rulesId.trim().length() == 0) {
240             return PluralRules.DEFAULT;
241         }
242         PluralRules rules = getRulesForRulesId(rulesId);
243         if (rules == null) {
244             rules = PluralRules.DEFAULT;
245         }
246         return rules;
247     }
248 
249     /**
250      * The only instance of the loader.
251      */
252     public static final PluralRulesLoader loader = new PluralRulesLoader();
253 
254     /* (non-Javadoc)
255      * @see com.ibm.icu.text.PluralRules.Factory#hasOverride(com.ibm.icu.util.ULocale)
256      */
257     @Override
hasOverride(ULocale locale)258     public boolean hasOverride(ULocale locale) {
259         return false;
260     }
261 
262     private static final PluralRanges UNKNOWN_RANGE = new PluralRanges().freeze();
263 
getPluralRanges(ULocale locale)264     public PluralRanges getPluralRanges(ULocale locale) {
265         // TODO markdavis Fix the bad fallback, here and elsewhere in this file.
266         String localeId = ULocale.canonicalize(locale.getBaseName());
267         PluralRanges result;
268         while (null == (result = localeIdToPluralRanges.get(localeId))) {
269             int ix = localeId.lastIndexOf("_");
270             if (ix == -1) {
271                 result = UNKNOWN_RANGE;
272                 break;
273             }
274             localeId = localeId.substring(0, ix);
275         }
276         return result;
277     }
278 
isPluralRangesAvailable(ULocale locale)279     public boolean isPluralRangesAvailable(ULocale locale) {
280         return getPluralRanges(locale) == UNKNOWN_RANGE;
281     }
282 
283     // TODO markdavis FIX HARD-CODED HACK once we have data from CLDR in the bundles
284     static {
285         String[][] pluralRangeData = {
286                 {"locales", "id ja km ko lo ms my th vi zh"},
287                 {"other", "other", "other"},
288 
289                 {"locales", "am bn fr gu hi hy kn mr pa zu"},
290                 {"one", "one", "one"},
291                 {"one", "other", "other"},
292                 {"other", "other", "other"},
293 
294                 {"locales", "fa"},
295                 {"one", "one", "other"},
296                 {"one", "other", "other"},
297                 {"other", "other", "other"},
298 
299                 {"locales", "ka"},
300                 {"one", "other", "one"},
301                 {"other", "one", "other"},
302                 {"other", "other", "other"},
303 
304                 {"locales", "az de el gl hu it kk ky ml mn ne nl pt sq sw ta te tr ug uz"},
305                 {"one", "other", "other"},
306                 {"other", "one", "one"},
307                 {"other", "other", "other"},
308 
309                 {"locales", "af bg ca en es et eu fi nb sv ur"},
310                 {"one", "other", "other"},
311                 {"other", "one", "other"},
312                 {"other", "other", "other"},
313 
314                 {"locales", "da fil is"},
315                 {"one", "one", "one"},
316                 {"one", "other", "other"},
317                 {"other", "one", "one"},
318                 {"other", "other", "other"},
319 
320                 {"locales", "si"},
321                 {"one", "one", "one"},
322                 {"one", "other", "other"},
323                 {"other", "one", "other"},
324                 {"other", "other", "other"},
325 
326                 {"locales", "mk"},
327                 {"one", "one", "other"},
328                 {"one", "other", "other"},
329                 {"other", "one", "other"},
330                 {"other", "other", "other"},
331 
332                 {"locales", "lv"},
333                 {"zero", "zero", "other"},
334                 {"zero", "one", "one"},
335                 {"zero", "other", "other"},
336                 {"one", "zero", "other"},
337                 {"one", "one", "one"},
338                 {"one", "other", "other"},
339                 {"other", "zero", "other"},
340                 {"other", "one", "one"},
341                 {"other", "other", "other"},
342 
343                 {"locales", "ro"},
344                 {"one", "few", "few"},
345                 {"one", "other", "other"},
346                 {"few", "one", "few"},
347                 {"few", "few", "few"},
348                 {"few", "other", "other"},
349                 {"other", "few", "few"},
350                 {"other", "other", "other"},
351 
352                 {"locales", "hr sr bs"},
353                 {"one", "one", "one"},
354                 {"one", "few", "few"},
355                 {"one", "other", "other"},
356                 {"few", "one", "one"},
357                 {"few", "few", "few"},
358                 {"few", "other", "other"},
359                 {"other", "one", "one"},
360                 {"other", "few", "few"},
361                 {"other", "other", "other"},
362 
363                 {"locales", "sl"},
364                 {"one", "one", "few"},
365                 {"one", "two", "two"},
366                 {"one", "few", "few"},
367                 {"one", "other", "other"},
368                 {"two", "one", "few"},
369                 {"two", "two", "two"},
370                 {"two", "few", "few"},
371                 {"two", "other", "other"},
372                 {"few", "one", "few"},
373                 {"few", "two", "two"},
374                 {"few", "few", "few"},
375                 {"few", "other", "other"},
376                 {"other", "one", "few"},
377                 {"other", "two", "two"},
378                 {"other", "few", "few"},
379                 {"other", "other", "other"},
380 
381                 {"locales", "he"},
382                 {"one", "two", "other"},
383                 {"one", "many", "many"},
384                 {"one", "other", "other"},
385                 {"two", "many", "other"},
386                 {"two", "other", "other"},
387                 {"many", "many", "many"},
388                 {"many", "other", "many"},
389                 {"other", "one", "other"},
390                 {"other", "two", "other"},
391                 {"other", "many", "many"},
392                 {"other", "other", "other"},
393 
394                 {"locales", "cs pl sk"},
395                 {"one", "few", "few"},
396                 {"one", "many", "many"},
397                 {"one", "other", "other"},
398                 {"few", "few", "few"},
399                 {"few", "many", "many"},
400                 {"few", "other", "other"},
401                 {"many", "one", "one"},
402                 {"many", "few", "few"},
403                 {"many", "many", "many"},
404                 {"many", "other", "other"},
405                 {"other", "one", "one"},
406                 {"other", "few", "few"},
407                 {"other", "many", "many"},
408                 {"other", "other", "other"},
409 
410                 {"locales", "lt ru uk"},
411                 {"one", "one", "one"},
412                 {"one", "few", "few"},
413                 {"one", "many", "many"},
414                 {"one", "other", "other"},
415                 {"few", "one", "one"},
416                 {"few", "few", "few"},
417                 {"few", "many", "many"},
418                 {"few", "other", "other"},
419                 {"many", "one", "one"},
420                 {"many", "few", "few"},
421                 {"many", "many", "many"},
422                 {"many", "other", "other"},
423                 {"other", "one", "one"},
424                 {"other", "few", "few"},
425                 {"other", "many", "many"},
426                 {"other", "other", "other"},
427 
428                 {"locales", "cy"},
429                 {"zero", "one", "one"},
430                 {"zero", "two", "two"},
431                 {"zero", "few", "few"},
432                 {"zero", "many", "many"},
433                 {"zero", "other", "other"},
434                 {"one", "two", "two"},
435                 {"one", "few", "few"},
436                 {"one", "many", "many"},
437                 {"one", "other", "other"},
438                 {"two", "few", "few"},
439                 {"two", "many", "many"},
440                 {"two", "other", "other"},
441                 {"few", "many", "many"},
442                 {"few", "other", "other"},
443                 {"many", "other", "other"},
444                 {"other", "one", "one"},
445                 {"other", "two", "two"},
446                 {"other", "few", "few"},
447                 {"other", "many", "many"},
448                 {"other", "other", "other"},
449 
450                 {"locales", "ar"},
451                 {"zero", "one", "zero"},
452                 {"zero", "two", "zero"},
453                 {"zero", "few", "few"},
454                 {"zero", "many", "many"},
455                 {"zero", "other", "other"},
456                 {"one", "two", "other"},
457                 {"one", "few", "few"},
458                 {"one", "many", "many"},
459                 {"one", "other", "other"},
460                 {"two", "few", "few"},
461                 {"two", "many", "many"},
462                 {"two", "other", "other"},
463                 {"few", "few", "few"},
464                 {"few", "many", "many"},
465                 {"few", "other", "other"},
466                 {"many", "few", "few"},
467                 {"many", "many", "many"},
468                 {"many", "other", "other"},
469                 {"other", "one", "other"},
470                 {"other", "two", "other"},
471                 {"other", "few", "few"},
472                 {"other", "many", "many"},
473                 {"other", "other", "other"},
474         };
475         PluralRanges pr = null;
476         String[] locales = null;
477         HashMap<String, PluralRanges> tempLocaleIdToPluralRanges = new HashMap<String, PluralRanges>();
478         for (String[] row : pluralRangeData) {
479             if (row[0].equals("locales")) {
480                 if (pr != null) {
pr.freeze()481                     pr.freeze();
482                     for (String locale : locales) {
tempLocaleIdToPluralRanges.put(locale, pr)483                         tempLocaleIdToPluralRanges.put(locale, pr);
484                     }
485                 }
486                 locales = row[1].split(" ");
487                 pr = new PluralRanges();
488             } else {
489                 pr.add(
StandardPlural.fromString(row[0])490                         StandardPlural.fromString(row[0]),
491                         StandardPlural.fromString(row[1]),
492                         StandardPlural.fromString(row[2]));
493             }
494         }
495         // do last one
496         for (String locale : locales) {
tempLocaleIdToPluralRanges.put(locale, pr)497             tempLocaleIdToPluralRanges.put(locale, pr);
498         }
499         // now make whole thing immutable
500         localeIdToPluralRanges = Collections.unmodifiableMap(tempLocaleIdToPluralRanges);
501     }
502 }