1 /*
2  * Copyright (c) 2012, 2020, 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 /*
27  * (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved
28  * (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved
29  *
30  * The original version of this source code and documentation
31  * is copyrighted and owned by Taligent, Inc., a wholly-owned
32  * subsidiary of IBM. These materials are provided under terms
33  * of a License Agreement between Taligent and Sun. This technology
34  * is protected by multiple US and International patents.
35  *
36  * This notice and attribution to Taligent may not be removed.
37  * Taligent is a registered trademark of Taligent, Inc.
38  *
39  */
40 
41 package sun.util.locale.provider;
42 
43 import java.lang.ref.ReferenceQueue;
44 import java.lang.ref.SoftReference;
45 import java.text.MessageFormat;
46 import java.text.NumberFormat;
47 import java.util.Arrays;
48 import java.util.Calendar;
49 import java.util.HashSet;
50 import java.util.LinkedHashSet;
51 import java.util.Locale;
52 import java.util.Objects;
53 import java.util.ResourceBundle;
54 import java.util.Set;
55 import java.util.TimeZone;
56 import java.util.concurrent.ConcurrentHashMap;
57 import java.util.concurrent.ConcurrentMap;
58 import sun.security.action.GetPropertyAction;
59 import sun.util.resources.LocaleData;
60 import sun.util.resources.OpenListResourceBundle;
61 import sun.util.resources.ParallelListResourceBundle;
62 import sun.util.resources.TimeZoneNamesBundle;
63 
64 /**
65  * Central accessor to locale-dependent resources for JRE/CLDR provider adapters.
66  *
67  * @author Masayoshi Okutsu
68  * @author Naoto Sato
69  */
70 public class LocaleResources {
71 
72     private final Locale locale;
73     private final LocaleData localeData;
74     private final LocaleProviderAdapter.Type type;
75 
76     // Resource cache
77     private final ConcurrentMap<String, ResourceReference> cache = new ConcurrentHashMap<>();
78     private final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
79 
80     // cache key prefixes
81     private static final String BREAK_ITERATOR_INFO = "BII.";
82     private static final String CALENDAR_DATA = "CALD.";
83     private static final String COLLATION_DATA_CACHEKEY = "COLD";
84     private static final String DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY = "DFSD";
85     private static final String CURRENCY_NAMES = "CN.";
86     private static final String LOCALE_NAMES = "LN.";
87     private static final String TIME_ZONE_NAMES = "TZN.";
88     private static final String ZONE_IDS_CACHEKEY = "ZID";
89     private static final String CALENDAR_NAMES = "CALN.";
90     private static final String NUMBER_PATTERNS_CACHEKEY = "NP";
91     private static final String COMPACT_NUMBER_PATTERNS_CACHEKEY = "CNP";
92     private static final String DATE_TIME_PATTERN = "DTP.";
93     private static final String RULES_CACHEKEY = "RULE";
94 
95     // TimeZoneNamesBundle exemplar city prefix
96     private static final String TZNB_EXCITY_PREFIX = "timezone.excity.";
97 
98     // null singleton cache value
99     private static final Object NULLOBJECT = new Object();
100 
LocaleResources(ResourceBundleBasedAdapter adapter, Locale locale)101     LocaleResources(ResourceBundleBasedAdapter adapter, Locale locale) {
102         this.locale = locale;
103         this.localeData = adapter.getLocaleData();
104         type = ((LocaleProviderAdapter)adapter).getAdapterType();
105     }
106 
removeEmptyReferences()107     private void removeEmptyReferences() {
108         Object ref;
109         while ((ref = referenceQueue.poll()) != null) {
110             cache.remove(((ResourceReference)ref).getCacheKey());
111         }
112     }
113 
getBreakIteratorInfo(String key)114     Object getBreakIteratorInfo(String key) {
115         Object biInfo;
116         String cacheKey = BREAK_ITERATOR_INFO + key;
117 
118         removeEmptyReferences();
119         ResourceReference data = cache.get(cacheKey);
120         if (data == null || ((biInfo = data.get()) == null)) {
121            biInfo = localeData.getBreakIteratorInfo(locale).getObject(key);
122            cache.put(cacheKey, new ResourceReference(cacheKey, biInfo, referenceQueue));
123         }
124 
125        return biInfo;
126     }
127 
getBreakIteratorResources(String key)128     byte[] getBreakIteratorResources(String key) {
129         return (byte[]) localeData.getBreakIteratorResources(locale).getObject(key);
130     }
131 
getCalendarData(String key)132     public String getCalendarData(String key) {
133         String caldata = "";
134         String cacheKey = CALENDAR_DATA  + key;
135 
136         removeEmptyReferences();
137 
138         ResourceReference data = cache.get(cacheKey);
139         if (data == null || ((caldata = (String) data.get()) == null)) {
140             ResourceBundle rb = localeData.getCalendarData(locale);
141             if (rb.containsKey(key)) {
142                 caldata = rb.getString(key);
143             }
144 
145             cache.put(cacheKey,
146                       new ResourceReference(cacheKey, caldata, referenceQueue));
147         }
148 
149         return caldata;
150     }
151 
getCollationData()152     public String getCollationData() {
153         String key = "Rule";
154         String coldata = "";
155 
156         removeEmptyReferences();
157         ResourceReference data = cache.get(COLLATION_DATA_CACHEKEY);
158         if (data == null || ((coldata = (String) data.get()) == null)) {
159             ResourceBundle rb = localeData.getCollationData(locale);
160             if (rb.containsKey(key)) {
161                 coldata = rb.getString(key);
162             }
163             cache.put(COLLATION_DATA_CACHEKEY,
164                       new ResourceReference(COLLATION_DATA_CACHEKEY, coldata, referenceQueue));
165         }
166 
167         return coldata;
168     }
169 
getDecimalFormatSymbolsData()170     public Object[] getDecimalFormatSymbolsData() {
171         Object[] dfsdata;
172 
173         removeEmptyReferences();
174         ResourceReference data = cache.get(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY);
175         if (data == null || ((dfsdata = (Object[]) data.get()) == null)) {
176             // Note that only dfsdata[0] is prepared here in this method. Other
177             // elements are provided by the caller, yet they are cached here.
178             ResourceBundle rb = localeData.getNumberFormatData(locale);
179             dfsdata = new Object[3];
180             dfsdata[0] = getNumberStrings(rb, "NumberElements");
181 
182             cache.put(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY,
183                       new ResourceReference(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY, dfsdata, referenceQueue));
184         }
185 
186         return dfsdata;
187     }
188 
getNumberStrings(ResourceBundle rb, String type)189     private String[] getNumberStrings(ResourceBundle rb, String type) {
190         String[] ret = null;
191         String key;
192         String numSys;
193 
194         // Number strings look up. First, try the Unicode extension
195         numSys = locale.getUnicodeLocaleType("nu");
196         if (numSys != null) {
197             key = numSys + "." + type;
198             if (rb.containsKey(key)) {
199                 ret = rb.getStringArray(key);
200             }
201         }
202 
203         // Next, try DefaultNumberingSystem value
204         if (ret == null && rb.containsKey("DefaultNumberingSystem")) {
205             key = rb.getString("DefaultNumberingSystem") + "." + type;
206             if (rb.containsKey(key)) {
207                 ret = rb.getStringArray(key);
208             }
209         }
210 
211         // Last resort. No need to check the availability.
212         // Just let it throw MissingResourceException when needed.
213         if (ret == null) {
214             ret = rb.getStringArray(type);
215         }
216 
217         return ret;
218     }
219 
getCurrencyName(String key)220     public String getCurrencyName(String key) {
221         Object currencyName = null;
222         String cacheKey = CURRENCY_NAMES + key;
223 
224         removeEmptyReferences();
225         ResourceReference data = cache.get(cacheKey);
226 
227         if (data != null && ((currencyName = data.get()) != null)) {
228             if (currencyName.equals(NULLOBJECT)) {
229                 currencyName = null;
230             }
231 
232             return (String) currencyName;
233         }
234 
235         OpenListResourceBundle olrb = localeData.getCurrencyNames(locale);
236 
237         if (olrb.containsKey(key)) {
238             currencyName = olrb.getObject(key);
239             cache.put(cacheKey,
240                       new ResourceReference(cacheKey, currencyName, referenceQueue));
241         }
242 
243         return (String) currencyName;
244     }
245 
getLocaleName(String key)246     public String getLocaleName(String key) {
247         Object localeName = null;
248         String cacheKey = LOCALE_NAMES + key;
249 
250         removeEmptyReferences();
251         ResourceReference data = cache.get(cacheKey);
252 
253         if (data != null && ((localeName = data.get()) != null)) {
254             if (localeName.equals(NULLOBJECT)) {
255                 localeName = null;
256             }
257 
258             return (String) localeName;
259         }
260 
261         OpenListResourceBundle olrb = localeData.getLocaleNames(locale);
262 
263         if (olrb.containsKey(key)) {
264             localeName = olrb.getObject(key);
265             cache.put(cacheKey,
266                       new ResourceReference(cacheKey, localeName, referenceQueue));
267         }
268 
269         return (String) localeName;
270     }
271 
getTimeZoneNames(String key)272     public Object getTimeZoneNames(String key) {
273         Object val = null;
274         String cacheKey = TIME_ZONE_NAMES + key;
275 
276         removeEmptyReferences();
277         ResourceReference data = cache.get(cacheKey);
278 
279         if (Objects.isNull(data) || Objects.isNull(val = data.get())) {
280             TimeZoneNamesBundle tznb = localeData.getTimeZoneNames(locale);
281             if (key.startsWith(TZNB_EXCITY_PREFIX)) {
282                 if (tznb.containsKey(key)) {
283                     val = tznb.getString(key);
284                     assert val instanceof String;
285                     trace("tznb: %s key: %s, val: %s\n", tznb, key, val);
286                 }
287             } else {
288                 String[] names = null;
289                 if (tznb.containsKey(key)) {
290                     names = tznb.getStringArray(key);
291                 } else {
292                     var tz = TimeZoneNameUtility.canonicalTZID(key).orElse(key);
293                     if (tznb.containsKey(tz)) {
294                         names = tznb.getStringArray(tz);
295                     }
296                 }
297 
298                 if (names != null) {
299                     names[0] = key;
300                     trace("tznb: %s key: %s, names: %s, %s, %s, %s, %s, %s, %s\n", tznb, key,
301                         names[0], names[1], names[2], names[3], names[4], names[5], names[6]);
302                     val = names;
303                 }
304             }
305             if (val != null) {
306                 cache.put(cacheKey,
307                           new ResourceReference(cacheKey, val, referenceQueue));
308             }
309         }
310 
311         return val;
312     }
313 
314     @SuppressWarnings("unchecked")
getZoneIDs()315     Set<String> getZoneIDs() {
316         Set<String> zoneIDs;
317 
318         removeEmptyReferences();
319         ResourceReference data = cache.get(ZONE_IDS_CACHEKEY);
320         if (data == null || ((zoneIDs = (Set<String>) data.get()) == null)) {
321             TimeZoneNamesBundle rb = localeData.getTimeZoneNames(locale);
322             zoneIDs = rb.keySet();
323             cache.put(ZONE_IDS_CACHEKEY,
324                       new ResourceReference(ZONE_IDS_CACHEKEY, zoneIDs, referenceQueue));
325         }
326 
327         return zoneIDs;
328     }
329 
330     // zoneStrings are cached separately in TimeZoneNameUtility.
getZoneStrings()331     String[][] getZoneStrings() {
332         TimeZoneNamesBundle rb = localeData.getTimeZoneNames(locale);
333         Set<String> keyset = getZoneIDs();
334         // Use a LinkedHashSet to preseve the order
335         Set<String[]> value = new LinkedHashSet<>();
336         Set<String> tzIds = new HashSet<>(Arrays.asList(TimeZone.getAvailableIDs()));
337         for (String key : keyset) {
338             if (!key.startsWith(TZNB_EXCITY_PREFIX)) {
339                 value.add(rb.getStringArray(key));
340                 tzIds.remove(key);
341             }
342         }
343 
344         if (type == LocaleProviderAdapter.Type.CLDR) {
345             // Note: TimeZoneNamesBundle creates a String[] on each getStringArray call.
346 
347             // Add timezones which are not present in this keyset,
348             // so that their fallback names will be generated at runtime.
349             tzIds.stream().filter(i -> (!i.startsWith("Etc/GMT")
350                     && !i.startsWith("GMT")
351                     && !i.startsWith("SystemV")))
352                     .forEach(tzid -> {
353                         String[] val = new String[7];
354                         if (keyset.contains(tzid)) {
355                             val = rb.getStringArray(tzid);
356                         } else {
357                             var canonID = TimeZoneNameUtility.canonicalTZID(tzid)
358                                             .orElse(tzid);
359                             if (keyset.contains(canonID)) {
360                                 val = rb.getStringArray(canonID);
361                             }
362                         }
363                         val[0] = tzid;
364                         value.add(val);
365                     });
366         }
367         return value.toArray(new String[0][]);
368     }
369 
getCalendarNames(String key)370     String[] getCalendarNames(String key) {
371         String[] names = null;
372         String cacheKey = CALENDAR_NAMES + key;
373 
374         removeEmptyReferences();
375         ResourceReference data = cache.get(cacheKey);
376 
377         if (data == null || ((names = (String[]) data.get()) == null)) {
378             ResourceBundle rb = localeData.getDateFormatData(locale);
379             if (rb.containsKey(key)) {
380                 names = rb.getStringArray(key);
381                 cache.put(cacheKey,
382                           new ResourceReference(cacheKey, names, referenceQueue));
383             }
384         }
385 
386         return names;
387     }
388 
getJavaTimeNames(String key)389     String[] getJavaTimeNames(String key) {
390         String[] names = null;
391         String cacheKey = CALENDAR_NAMES + key;
392 
393         removeEmptyReferences();
394         ResourceReference data = cache.get(cacheKey);
395 
396         if (data == null || ((names = (String[]) data.get()) == null)) {
397             ResourceBundle rb = getJavaTimeFormatData();
398             if (rb.containsKey(key)) {
399                 names = rb.getStringArray(key);
400                 cache.put(cacheKey,
401                           new ResourceReference(cacheKey, names, referenceQueue));
402             }
403         }
404 
405         return names;
406     }
407 
getDateTimePattern(int timeStyle, int dateStyle, Calendar cal)408     public String getDateTimePattern(int timeStyle, int dateStyle, Calendar cal) {
409         if (cal == null) {
410             cal = Calendar.getInstance(locale);
411         }
412         return getDateTimePattern(null, timeStyle, dateStyle, cal.getCalendarType());
413     }
414 
415     /**
416      * Returns a date-time format pattern
417      * @param timeStyle style of time; one of FULL, LONG, MEDIUM, SHORT in DateFormat,
418      *                  or -1 if not required
419      * @param dateStyle style of time; one of FULL, LONG, MEDIUM, SHORT in DateFormat,
420      *                  or -1 if not required
421      * @param calType   the calendar type for the pattern
422      * @return the pattern string
423      */
getJavaTimeDateTimePattern(int timeStyle, int dateStyle, String calType)424     public String getJavaTimeDateTimePattern(int timeStyle, int dateStyle, String calType) {
425         calType = CalendarDataUtility.normalizeCalendarType(calType);
426         String pattern;
427         pattern = getDateTimePattern("java.time.", timeStyle, dateStyle, calType);
428         if (pattern == null) {
429             pattern = getDateTimePattern(null, timeStyle, dateStyle, calType);
430         }
431         return pattern;
432     }
433 
getDateTimePattern(String prefix, int timeStyle, int dateStyle, String calType)434     private String getDateTimePattern(String prefix, int timeStyle, int dateStyle, String calType) {
435         String pattern;
436         String timePattern = null;
437         String datePattern = null;
438 
439         if (timeStyle >= 0) {
440             if (prefix != null) {
441                 timePattern = getDateTimePattern(prefix, "TimePatterns", timeStyle, calType);
442             }
443             if (timePattern == null) {
444                 timePattern = getDateTimePattern(null, "TimePatterns", timeStyle, calType);
445             }
446         }
447         if (dateStyle >= 0) {
448             if (prefix != null) {
449                 datePattern = getDateTimePattern(prefix, "DatePatterns", dateStyle, calType);
450             }
451             if (datePattern == null) {
452                 datePattern = getDateTimePattern(null, "DatePatterns", dateStyle, calType);
453             }
454         }
455         if (timeStyle >= 0) {
456             if (dateStyle >= 0) {
457                 String dateTimePattern = null;
458                 int dateTimeStyle = Math.max(dateStyle, timeStyle);
459                 if (prefix != null) {
460                     dateTimePattern = getDateTimePattern(prefix, "DateTimePatterns", dateTimeStyle, calType);
461                 }
462                 if (dateTimePattern == null) {
463                     dateTimePattern = getDateTimePattern(null, "DateTimePatterns", dateTimeStyle, calType);
464                 }
465                 pattern = switch (Objects.requireNonNull(dateTimePattern)) {
466                     case "{1} {0}" -> datePattern + " " + timePattern;
467                     case "{0} {1}" -> timePattern + " " + datePattern;
468                     default -> MessageFormat.format(dateTimePattern.replaceAll("'", "''"), timePattern, datePattern);
469                 };
470             } else {
471                 pattern = timePattern;
472             }
473         } else if (dateStyle >= 0) {
474             pattern = datePattern;
475         } else {
476             throw new IllegalArgumentException("No date or time style specified");
477         }
478         return pattern;
479     }
480 
getNumberPatterns()481     public String[] getNumberPatterns() {
482         String[] numberPatterns;
483 
484         removeEmptyReferences();
485         ResourceReference data = cache.get(NUMBER_PATTERNS_CACHEKEY);
486 
487         if (data == null || ((numberPatterns = (String[]) data.get()) == null)) {
488             ResourceBundle resource = localeData.getNumberFormatData(locale);
489             numberPatterns = getNumberStrings(resource, "NumberPatterns");
490             cache.put(NUMBER_PATTERNS_CACHEKEY,
491                       new ResourceReference(NUMBER_PATTERNS_CACHEKEY, numberPatterns, referenceQueue));
492         }
493 
494         return numberPatterns;
495     }
496 
497     /**
498      * Returns the compact number format patterns.
499      * @param formatStyle the style for formatting a number
500      * @return an array of compact number patterns
501      */
getCNPatterns(NumberFormat.Style formatStyle)502     public String[] getCNPatterns(NumberFormat.Style formatStyle) {
503 
504         Objects.requireNonNull(formatStyle);
505         String[] compactNumberPatterns;
506         removeEmptyReferences();
507         String width = (formatStyle == NumberFormat.Style.LONG) ? "long" : "short";
508         String cacheKey = width + "." + COMPACT_NUMBER_PATTERNS_CACHEKEY;
509         ResourceReference data = cache.get(cacheKey);
510         if (data == null || ((compactNumberPatterns
511                 = (String[]) data.get()) == null)) {
512             ResourceBundle resource = localeData.getNumberFormatData(locale);
513             compactNumberPatterns = (String[]) resource
514                     .getObject(width + ".CompactNumberPatterns");
515             cache.put(cacheKey, new ResourceReference(cacheKey, compactNumberPatterns, referenceQueue));
516         }
517         return compactNumberPatterns;
518     }
519 
520 
521     /**
522      * Returns the FormatData resource bundle of this LocaleResources.
523      * The FormatData should be used only for accessing extra
524      * resources required by JSR 310.
525      */
getJavaTimeFormatData()526     public ResourceBundle getJavaTimeFormatData() {
527         ResourceBundle rb = localeData.getDateFormatData(locale);
528         if (rb instanceof ParallelListResourceBundle) {
529             localeData.setSupplementary((ParallelListResourceBundle) rb);
530         }
531         return rb;
532     }
533 
getDateTimePattern(String prefix, String key, int styleIndex, String calendarType)534     private String getDateTimePattern(String prefix, String key, int styleIndex, String calendarType) {
535         StringBuilder sb = new StringBuilder();
536         if (prefix != null) {
537             sb.append(prefix);
538         }
539         if (!"gregory".equals(calendarType)) {
540             sb.append(calendarType).append('.');
541         }
542         sb.append(key);
543         String resourceKey = sb.toString();
544         String cacheKey = sb.insert(0, DATE_TIME_PATTERN).toString();
545 
546         removeEmptyReferences();
547         ResourceReference data = cache.get(cacheKey);
548         Object value = NULLOBJECT;
549 
550         if (data == null || ((value = data.get()) == null)) {
551             ResourceBundle r = (prefix != null) ? getJavaTimeFormatData() : localeData.getDateFormatData(locale);
552             if (r.containsKey(resourceKey)) {
553                 value = r.getStringArray(resourceKey);
554             } else {
555                 assert !resourceKey.equals(key);
556                 if (r.containsKey(key)) {
557                     value = r.getStringArray(key);
558                 }
559             }
560             cache.put(cacheKey,
561                       new ResourceReference(cacheKey, value, referenceQueue));
562         }
563         if (value == NULLOBJECT) {
564             assert prefix != null;
565             return null;
566         }
567 
568         // for DateTimePatterns. CLDR has multiple styles, while JRE has one.
569         String[] styles = (String[])value;
570         return (styles.length > 1 ? styles[styleIndex] : styles[0]);
571     }
572 
getRules()573     public String[] getRules() {
574         String[] rules;
575 
576         removeEmptyReferences();
577         ResourceReference data = cache.get(RULES_CACHEKEY);
578 
579         if (data == null || ((rules = (String[]) data.get()) == null)) {
580             ResourceBundle rb = localeData.getDateFormatData(locale);
581             rules = new String[2];
582             rules[0] = rules[1] = "";
583             if (rb.containsKey("PluralRules")) {
584                 rules[0] = rb.getString("PluralRules");
585             }
586             if (rb.containsKey("DayPeriodRules")) {
587                 rules[1] = rb.getString("DayPeriodRules");
588             }
589             cache.put(RULES_CACHEKEY, new ResourceReference(RULES_CACHEKEY, rules, referenceQueue));
590         }
591 
592         return rules;
593     }
594 
595     private static class ResourceReference extends SoftReference<Object> {
596         private final String cacheKey;
597 
ResourceReference(String cacheKey, Object o, ReferenceQueue<Object> q)598         ResourceReference(String cacheKey, Object o, ReferenceQueue<Object> q) {
599             super(o, q);
600             this.cacheKey = cacheKey;
601         }
602 
getCacheKey()603         String getCacheKey() {
604             return cacheKey;
605         }
606     }
607 
608     private static final boolean TRACE_ON = Boolean.valueOf(
609         GetPropertyAction.privilegedGetProperty("locale.resources.debug", "false"));
610 
trace(String format, Object... params)611     public static void trace(String format, Object... params) {
612         if (TRACE_ON) {
613             System.out.format(format, params);
614         }
615     }
616 }
617