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