1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 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.LinkedHashSet; 16 import java.util.Map; 17 import java.util.MissingResourceException; 18 import java.util.Set; 19 import java.util.TreeMap; 20 21 import com.ibm.icu.impl.number.range.StandardPluralRanges; 22 import com.ibm.icu.text.PluralRules; 23 import com.ibm.icu.text.PluralRules.PluralType; 24 import com.ibm.icu.util.ULocale; 25 import com.ibm.icu.util.UResourceBundle; 26 27 /** 28 * Loader for plural rules data. 29 */ 30 public class PluralRulesLoader extends PluralRules.Factory { 31 // Key is rules set + ranges set 32 private final Map<String, PluralRules> pluralRulesCache; 33 // lazy init, use getLocaleIdToRulesIdMap to access 34 private Map<String, String> localeIdToCardinalRulesId; 35 private Map<String, String> localeIdToOrdinalRulesId; 36 private Map<String, ULocale> rulesIdToEquivalentULocale; 37 38 39 /** 40 * Access through singleton. 41 */ PluralRulesLoader()42 private PluralRulesLoader() { 43 pluralRulesCache = new HashMap<String, PluralRules>(); 44 } 45 46 /** 47 * Returns the locales for which we have plurals data. Utility for testing. 48 */ getAvailableULocales()49 public ULocale[] getAvailableULocales() { 50 Set<String> keys = getLocaleIdToRulesIdMap(PluralType.CARDINAL).keySet(); 51 Set<ULocale> locales = new LinkedHashSet<ULocale>(keys.size()); 52 for (Iterator<String> iter = keys.iterator(); iter.hasNext();) { 53 locales.add(ULocale.createCanonical(iter.next())); 54 } 55 return locales.toArray(new ULocale[0]); 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 */ getOrCreateRulesForLocale(ULocale locale, PluralRules.PluralType type)182 public PluralRules getOrCreateRulesForLocale(ULocale locale, PluralRules.PluralType type) { 183 String rulesId = getRulesIdForLocale(locale, type); 184 if (rulesId == null || rulesId.trim().length() == 0) { 185 return null; 186 } 187 String rangesId = StandardPluralRanges.getSetForLocale(locale); 188 String cacheKey = rulesId + "/" + rangesId; // could end with "/null" (this is OK) 189 // synchronize on the map. release the lock temporarily while we build the rules. 190 PluralRules rules = null; 191 boolean hasRules; // Separate boolean because stored rules can be null. 192 synchronized (pluralRulesCache) { 193 hasRules = pluralRulesCache.containsKey(cacheKey); 194 if (hasRules) { 195 rules = pluralRulesCache.get(cacheKey); // can be null 196 } 197 } 198 if (!hasRules) { 199 try { 200 UResourceBundle pluralb = getPluralBundle(); 201 UResourceBundle rulesb = pluralb.get("rules"); 202 UResourceBundle setb = rulesb.get(rulesId); 203 204 StringBuilder sb = new StringBuilder(); 205 for (int i = 0; i < setb.getSize(); ++i) { 206 UResourceBundle b = setb.get(i); 207 if (i > 0) { 208 sb.append("; "); 209 } 210 sb.append(b.getKey()); 211 sb.append(": "); 212 sb.append(b.getString()); 213 } 214 StandardPluralRanges ranges = StandardPluralRanges.forSet(rangesId); 215 rules = PluralRules.newInternal(sb.toString(), ranges); 216 } catch (ParseException e) { 217 } catch (MissingResourceException e) { 218 } 219 synchronized (pluralRulesCache) { 220 if (pluralRulesCache.containsKey(cacheKey)) { 221 rules = pluralRulesCache.get(cacheKey); 222 } else { 223 pluralRulesCache.put(cacheKey, rules); // can be null 224 } 225 } 226 } 227 return rules; 228 } 229 230 /** 231 * Return the plurals resource. Note MissingResourceException is unchecked, 232 * listed here for clarity. Callers should handle this exception. 233 */ getPluralBundle()234 public UResourceBundle getPluralBundle() throws MissingResourceException { 235 return ICUResourceBundle.getBundleInstance( 236 ICUData.ICU_BASE_NAME, "plurals", 237 ICUResourceBundle.ICU_DATA_CLASS_LOADER, true); 238 } 239 240 /** 241 * Returns the plural rules for the the locale. If we don't have data, 242 * com.ibm.icu.text.PluralRules.DEFAULT is returned. 243 */ forLocale(ULocale locale, PluralRules.PluralType type)244 public PluralRules forLocale(ULocale locale, PluralRules.PluralType type) { 245 PluralRules rules = getOrCreateRulesForLocale(locale, type); 246 if (rules == null) { 247 rules = PluralRules.DEFAULT; 248 } 249 return rules; 250 } 251 252 /** 253 * The only instance of the loader. 254 */ 255 public static final PluralRulesLoader loader = new PluralRulesLoader(); 256 257 /* (non-Javadoc) 258 * @see com.ibm.icu.text.PluralRules.Factory#hasOverride(com.ibm.icu.util.ULocale) 259 */ 260 @Override hasOverride(ULocale locale)261 public boolean hasOverride(ULocale locale) { 262 return false; 263 } 264 } 265