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