1 /* 2 * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.util.cldr; 27 28 import java.security.AccessController; 29 import java.security.PrivilegedAction; 30 import java.security.PrivilegedActionException; 31 import java.security.PrivilegedExceptionAction; 32 import java.text.spi.BreakIteratorProvider; 33 import java.text.spi.CollatorProvider; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.HashSet; 37 import java.util.List; 38 import java.util.Locale; 39 import java.util.Map; 40 import java.util.Optional; 41 import java.util.ServiceLoader; 42 import java.util.Set; 43 import java.util.StringTokenizer; 44 import java.util.concurrent.ConcurrentHashMap; 45 import java.util.spi.CalendarDataProvider; 46 import java.util.spi.CalendarNameProvider; 47 import java.util.spi.TimeZoneNameProvider; 48 import sun.util.locale.provider.JRELocaleProviderAdapter; 49 import sun.util.locale.provider.LocaleDataMetaInfo; 50 import sun.util.locale.provider.LocaleProviderAdapter; 51 52 /** 53 * LocaleProviderAdapter implementation for the CLDR locale data. 54 * 55 * @author Masayoshi Okutsu 56 * @author Naoto Sato 57 */ 58 public class CLDRLocaleProviderAdapter extends JRELocaleProviderAdapter { 59 60 private static final CLDRBaseLocaleDataMetaInfo baseMetaInfo = new CLDRBaseLocaleDataMetaInfo(); 61 // Assumption: CLDR has only one non-Base module. 62 private final LocaleDataMetaInfo nonBaseMetaInfo; 63 64 // parent locales map 65 private static volatile Map<Locale, Locale> parentLocalesMap; 66 // language aliases map 67 private static volatile Map<String,String> langAliasesMap; 68 // cache to hold locale to locale mapping for language aliases. 69 private static final Map<Locale, Locale> langAliasesCache; 70 static { 71 parentLocalesMap = new ConcurrentHashMap<>(); 72 langAliasesMap = new ConcurrentHashMap<>(); 73 langAliasesCache = new ConcurrentHashMap<>(); 74 // Assuming these locales do NOT have irregular parent locales. parentLocalesMap.put(Locale.ROOT, Locale.ROOT)75 parentLocalesMap.put(Locale.ROOT, Locale.ROOT); parentLocalesMap.put(Locale.ENGLISH, Locale.ENGLISH)76 parentLocalesMap.put(Locale.ENGLISH, Locale.ENGLISH); parentLocalesMap.put(Locale.US, Locale.US)77 parentLocalesMap.put(Locale.US, Locale.US); 78 } 79 80 @SuppressWarnings("removal") CLDRLocaleProviderAdapter()81 public CLDRLocaleProviderAdapter() { 82 LocaleDataMetaInfo nbmi; 83 84 try { 85 nbmi = AccessController.doPrivileged((PrivilegedExceptionAction<LocaleDataMetaInfo>) () -> { 86 for (LocaleDataMetaInfo ldmi : ServiceLoader.loadInstalled(LocaleDataMetaInfo.class)) { 87 if (ldmi.getType() == Type.CLDR) { 88 return ldmi; 89 } 90 } 91 return null; 92 }); 93 } catch (PrivilegedActionException pae) { 94 throw new InternalError(pae.getCause()); 95 } 96 97 nonBaseMetaInfo = nbmi; 98 } 99 100 /** 101 * Returns the type of this LocaleProviderAdapter 102 * @return the type of this 103 */ 104 @Override getAdapterType()105 public LocaleProviderAdapter.Type getAdapterType() { 106 return LocaleProviderAdapter.Type.CLDR; 107 } 108 109 @Override getBreakIteratorProvider()110 public BreakIteratorProvider getBreakIteratorProvider() { 111 return null; 112 } 113 114 @Override getCalendarDataProvider()115 public CalendarDataProvider getCalendarDataProvider() { 116 if (calendarDataProvider == null) { 117 @SuppressWarnings("removal") 118 CalendarDataProvider provider = AccessController.doPrivileged( 119 (PrivilegedAction<CalendarDataProvider>) () -> 120 new CLDRCalendarDataProviderImpl( 121 getAdapterType(), 122 getLanguageTagSet("CalendarData"))); 123 124 synchronized (this) { 125 if (calendarDataProvider == null) { 126 calendarDataProvider = provider; 127 } 128 } 129 } 130 return calendarDataProvider; 131 } 132 133 @Override getCalendarNameProvider()134 public CalendarNameProvider getCalendarNameProvider() { 135 if (calendarNameProvider == null) { 136 @SuppressWarnings("removal") 137 CalendarNameProvider provider = AccessController.doPrivileged( 138 (PrivilegedAction<CalendarNameProvider>) () 139 -> new CLDRCalendarNameProviderImpl( 140 getAdapterType(), 141 getLanguageTagSet("FormatData"))); 142 143 synchronized (this) { 144 if (calendarNameProvider == null) { 145 calendarNameProvider = provider; 146 } 147 } 148 } 149 return calendarNameProvider; 150 } 151 152 @Override getCollatorProvider()153 public CollatorProvider getCollatorProvider() { 154 return null; 155 } 156 157 @Override getTimeZoneNameProvider()158 public TimeZoneNameProvider getTimeZoneNameProvider() { 159 if (timeZoneNameProvider == null) { 160 @SuppressWarnings("removal") 161 TimeZoneNameProvider provider = AccessController.doPrivileged( 162 (PrivilegedAction<TimeZoneNameProvider>) () -> 163 new CLDRTimeZoneNameProviderImpl( 164 getAdapterType(), 165 getLanguageTagSet("TimeZoneNames"))); 166 167 synchronized (this) { 168 if (timeZoneNameProvider == null) { 169 timeZoneNameProvider = provider; 170 } 171 } 172 } 173 return timeZoneNameProvider; 174 } 175 176 @Override getAvailableLocales()177 public Locale[] getAvailableLocales() { 178 Set<String> all = createLanguageTagSet("AvailableLocales"); 179 Locale[] locs = new Locale[all.size()]; 180 int index = 0; 181 for (String tag : all) { 182 locs[index++] = Locale.forLanguageTag(tag); 183 } 184 return locs; 185 } 186 applyAliases(Locale loc)187 private static Locale applyAliases(Locale loc) { 188 if (langAliasesMap.isEmpty()) { 189 langAliasesMap = baseMetaInfo.getLanguageAliasMap(); 190 } 191 Locale locale = langAliasesCache.get(loc); 192 if (locale == null) { 193 String locTag = loc.toLanguageTag(); 194 Locale aliasLocale = langAliasesMap.containsKey(locTag) 195 ? Locale.forLanguageTag(langAliasesMap.get(locTag)) : loc; 196 langAliasesCache.putIfAbsent(loc, aliasLocale); 197 return aliasLocale; 198 } else { 199 return locale; 200 } 201 } 202 203 @Override createLanguageTagSet(String category)204 protected Set<String> createLanguageTagSet(String category) { 205 // Assume all categories support the same set as AvailableLocales 206 // in CLDR adapter. 207 category = "AvailableLocales"; 208 209 // Directly call Base tags, as we know it's in the base module. 210 String supportedLocaleString = baseMetaInfo.availableLanguageTags(category); 211 String nonBaseTags = null; 212 213 if (nonBaseMetaInfo != null) { 214 nonBaseTags = nonBaseMetaInfo.availableLanguageTags(category); 215 } 216 if (nonBaseTags != null) { 217 if (supportedLocaleString != null) { 218 supportedLocaleString += " " + nonBaseTags; 219 } else { 220 supportedLocaleString = nonBaseTags; 221 } 222 } 223 if (supportedLocaleString == null) { 224 return Collections.emptySet(); 225 } 226 StringTokenizer tokens = new StringTokenizer(supportedLocaleString); 227 Set<String> tagset = new HashSet<>((tokens.countTokens() * 4 + 2) / 3); 228 while (tokens.hasMoreTokens()) { 229 tagset.add(tokens.nextToken()); 230 } 231 return tagset; 232 } 233 234 // Implementation of ResourceBundleBasedAdapter 235 @Override getCandidateLocales(String baseName, Locale locale)236 public List<Locale> getCandidateLocales(String baseName, Locale locale) { 237 List<Locale> candidates = super.getCandidateLocales(baseName, applyAliases(locale)); 238 return applyParentLocales(baseName, candidates); 239 } 240 applyParentLocales(String baseName, List<Locale> candidates)241 private List<Locale> applyParentLocales(String baseName, List<Locale> candidates) { 242 // check irregular parents 243 for (int i = 0; i < candidates.size(); i++) { 244 Locale l = candidates.get(i); 245 if (!l.equals(Locale.ROOT)) { 246 Locale p = getParentLocale(l); 247 if (p != null && 248 !candidates.get(i+1).equals(p)) { 249 List<Locale> applied = candidates.subList(0, i+1); 250 if (applied.contains(p)) { 251 // avoid circular recursion (could happen with nb/no case) 252 continue; 253 } 254 applied.addAll(applyParentLocales(baseName, super.getCandidateLocales(baseName, p))); 255 return applied; 256 } 257 } 258 } 259 260 return candidates; 261 } 262 getParentLocale(Locale locale)263 private static Locale getParentLocale(Locale locale) { 264 Locale parent = parentLocalesMap.get(locale); 265 266 if (parent == null) { 267 String tag = locale.toLanguageTag(); 268 for (Map.Entry<Locale, String[]> entry : baseMetaInfo.parentLocales().entrySet()) { 269 if (Arrays.binarySearch(entry.getValue(), tag) >= 0) { 270 parent = entry.getKey(); 271 break; 272 } 273 } 274 if (parent == null) { 275 parent = locale; // non existent marker 276 } 277 parentLocalesMap.putIfAbsent(locale, parent); 278 } 279 280 if (locale.equals(parent)) { 281 // means no irregular parent. 282 parent = null; 283 } 284 285 return parent; 286 } 287 288 /** 289 * This method returns equivalent CLDR supported locale 290 * for no, no-NO locales so that COMPAT locales do not precede 291 * those locales during ResourceBundle search path, also if an alias exists for a locale, 292 * it returns equivalent locale, e.g for zh_HK it returns zh_Hant-HK. 293 */ getEquivalentLoc(Locale locale)294 private static Locale getEquivalentLoc(Locale locale) { 295 return switch (locale.toString()) { 296 case "no", "no_NO" -> Locale.forLanguageTag("nb"); 297 default -> applyAliases(locale); 298 }; 299 } 300 301 @Override isSupportedProviderLocale(Locale locale, Set<String> langtags)302 public boolean isSupportedProviderLocale(Locale locale, Set<String> langtags) { 303 return Locale.ROOT.equals(locale) 304 || langtags.contains(locale.stripExtensions().toLanguageTag()) 305 || langtags.contains(getEquivalentLoc(locale).toLanguageTag()); 306 } 307 308 /** 309 * Returns the canonical ID for the given ID 310 */ canonicalTZID(String id)311 public Optional<String> canonicalTZID(String id) { 312 return Optional.ofNullable(baseMetaInfo.tzCanonicalIDs().get(id)); 313 } 314 } 315