1 /*
   2  * Copyright (c) 2005, 2017, 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.locale.provider;
  27 
  28 import java.lang.ref.SoftReference;
  29 import java.util.LinkedList;
  30 import java.util.List;
  31 import java.util.Locale;
  32 import java.util.Map;
  33 import java.util.Objects;
  34 import java.util.Optional;
  35 import java.util.Set;
  36 import java.util.concurrent.ConcurrentHashMap;
  37 import java.util.spi.TimeZoneNameProvider;
  38 import sun.util.calendar.ZoneInfo;
  39 import sun.util.cldr.CLDRLocaleProviderAdapter;
  40 import static sun.util.locale.provider.LocaleProviderAdapter.Type;
  41 
  42 /**
  43  * Utility class that deals with the localized time zone names
  44  *
  45  * @author Naoto Sato
  46  * @author Masayoshi Okutsu
  47  */
  48 public final class TimeZoneNameUtility {
  49 
  50     /**
  51      * cache to hold time zone localized strings. Keyed by Locale
  52      */
  53     private static final ConcurrentHashMap<Locale, SoftReference<String[][]>> cachedZoneData =
  54         new ConcurrentHashMap<>();
  55 
  56     /**
  57      * Cache for managing display names per timezone per locale
  58      * The structure is:
  59      *     Map(key=id, value=SoftReference(Map(key=locale, value=displaynames)))
  60      */
  61     private static final Map<String, SoftReference<Map<Locale, String[]>>> cachedDisplayNames =
  62         new ConcurrentHashMap<>();
  63 
  64     /**
  65      * get time zone localized strings. Enumerate all keys.
  66      */
  67     public static String[][] getZoneStrings(Locale locale) {
  68         String[][] zones;
  69         SoftReference<String[][]> data = cachedZoneData.get(locale);
  70 
  71         if (data == null || ((zones = data.get()) == null)) {
  72             zones = loadZoneStrings(locale);
  73             data = new SoftReference<>(zones);
  74             cachedZoneData.put(locale, data);
  75         }
  76 
  77         return zones;
  78     }
  79 
  80     private static String[][] loadZoneStrings(Locale locale) {
  81         // If the provider is a TimeZoneNameProviderImpl, call its getZoneStrings
  82         // in order to avoid per-ID retrieval.
  83         LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(TimeZoneNameProvider.class, locale);
  84         TimeZoneNameProvider provider = adapter.getTimeZoneNameProvider();
  85         if (provider instanceof TimeZoneNameProviderImpl) {
  86             String[][] zoneStrings = ((TimeZoneNameProviderImpl)provider).getZoneStrings(locale);
  87 
  88             if (zoneStrings.length == 0 && locale.equals(Locale.ROOT)) {
  89                 // Unlike other *Name provider, zoneStrings search won't do the fallback
  90                 // name search. If the ResourceBundle found for the root locale contains no
  91                 // zoneStrings, just use the one for English, assuming English bundle
  92                 // contains all the tzids and their names.
  93                 zoneStrings= getZoneStrings(Locale.ENGLISH);
  94             }
  95 
  96             return zoneStrings;
  97         }
  98 
  99         // Performs per-ID retrieval.
 100         Set<String> zoneIDs = LocaleProviderAdapter.forJRE().getLocaleResources(locale).getZoneIDs();
 101         List<String[]> zones = new LinkedList<>();
 102         for (String key : zoneIDs) {
 103             String[] names = retrieveDisplayNamesImpl(key, locale);
 104             if (names != null) {
 105                 zones.add(names);
 106             }
 107         }
 108 
 109         String[][] zonesArray = new String[zones.size()][];
 110         return zones.toArray(zonesArray);
 111     }
 112 
 113     /**
 114      * Retrieve display names for a time zone ID.
 115      */
 116     public static String[] retrieveDisplayNames(String id, Locale locale) {
 117         Objects.requireNonNull(id);
 118         Objects.requireNonNull(locale);
 119 
 120         return retrieveDisplayNamesImpl(id, locale);
 121     }
 122 
 123     /**
 124      * Retrieves a generic time zone display name for a time zone ID.
 125      *
 126      * @param id     time zone ID
 127      * @param style  TimeZone.LONG or TimeZone.SHORT
 128      * @param locale desired Locale
 129      * @return the requested generic time zone display name, or null if not found.
 130      */
 131     public static String retrieveGenericDisplayName(String id, int style, Locale locale) {
 132         String[] names = retrieveDisplayNamesImpl(id, locale);
 133         if (Objects.nonNull(names)) {
 134             return names[6 - style];
 135         } else {
 136             return null;
 137         }
 138     }
 139 
 140     /**
 141      * Retrieves a standard or daylight-saving time name for the given time zone ID.
 142      *
 143      * @param id       time zone ID
 144      * @param daylight true for a daylight saving time name, or false for a standard time name
 145      * @param style    TimeZone.LONG or TimeZone.SHORT
 146      * @param locale   desired Locale
 147      * @return the requested time zone name, or null if not found.
 148      */
 149     public static String retrieveDisplayName(String id, boolean daylight, int style, Locale locale) {
 150         String[] names = retrieveDisplayNamesImpl(id, locale);
 151         if (Objects.nonNull(names)) {
 152             return names[(daylight ? 4 : 2) - style];
 153         } else {
 154             return null;
 155         }
 156     }
 157 
 158     /**
 159      * Converts the time zone id from LDML's 5-letter id to tzdb's id
 160      *
 161      * @param shortID       time zone short ID defined in LDML
 162      * @return the tzdb's time zone ID
 163      */
 164     public static Optional<String> convertLDMLShortID(String shortID) {
 165         return ((CLDRLocaleProviderAdapter)LocaleProviderAdapter.forType(Type.CLDR))
 166                     .getTimeZoneID(shortID)
 167                     .map(id -> id.replaceAll("\\s.*", ""));
 168     }
 169 
 170     private static String[] retrieveDisplayNamesImpl(String id, Locale locale) {
 171         LocaleServiceProviderPool pool =
 172             LocaleServiceProviderPool.getPool(TimeZoneNameProvider.class);
 173         String[] names;
 174         Map<Locale, String[]> perLocale = null;
 175 
 176         SoftReference<Map<Locale, String[]>> ref = cachedDisplayNames.get(id);
 177         if (Objects.nonNull(ref)) {
 178             perLocale = ref.get();
 179             if (Objects.nonNull(perLocale)) {
 180                 names = perLocale.get(locale);
 181                 if (Objects.nonNull(names)) {
 182                     return names;
 183                 }
 184             }
 185         }
 186 
 187         // build names array
 188         names = new String[7];
 189         names[0] = id;
 190         for (int i = 1; i <= 6; i ++) {
 191             names[i] = pool.getLocalizedObject(TimeZoneNameGetter.INSTANCE, locale,
 192                     i<5 ? (i<3 ? "std" : "dst") : "generic", i%2, id);
 193         }
 194 
 195         if (Objects.isNull(perLocale)) {
 196             perLocale = new ConcurrentHashMap<>();
 197         }
 198         perLocale.put(locale, names);
 199         ref = new SoftReference<>(perLocale);
 200         cachedDisplayNames.put(id, ref);
 201         return names;
 202     }
 203 
 204 
 205     /**
 206      * Obtains a localized time zone strings from a TimeZoneNameProvider
 207      * implementation.
 208      */
 209     private static class TimeZoneNameGetter
 210         implements LocaleServiceProviderPool.LocalizedObjectGetter<TimeZoneNameProvider,
 211                                                                    String> {
 212         private static final TimeZoneNameGetter INSTANCE =
 213             new TimeZoneNameGetter();
 214 
 215         @Override
 216         public String getObject(TimeZoneNameProvider timeZoneNameProvider,
 217                                 Locale locale,
 218                                 String requestID,
 219                                 Object... params) {
 220             assert params.length == 2;
 221             int style = (int) params[0];
 222             String tzid = (String) params[1];
 223             String value = getName(timeZoneNameProvider, locale, requestID, style, tzid);
 224             if (value == null) {
 225                 Map<String, String> aliases = ZoneInfo.getAliasTable();
 226                 if (aliases != null) {
 227                     String canonicalID = aliases.get(tzid);
 228                     if (canonicalID != null) {
 229                         value = getName(timeZoneNameProvider, locale, requestID, style, canonicalID);
 230                     }
 231                     if (value == null) {
 232                         value = examineAliases(timeZoneNameProvider, locale, requestID,
 233                                      canonicalID != null ? canonicalID : tzid, style, aliases);
 234                     }
 235                 }
 236             }
 237 
 238             return value;
 239         }
 240 
 241         private static String examineAliases(TimeZoneNameProvider tznp, Locale locale,
 242                                              String requestID, String tzid, int style,
 243                                              Map<String, String> aliases) {
 244             for (Map.Entry<String, String> entry : aliases.entrySet()) {
 245                 if (entry.getValue().equals(tzid)) {
 246                     String alias = entry.getKey();
 247                     String name = getName(tznp, locale, requestID, style, alias);
 248                     if (name != null) {
 249                         return name;
 250                     }
 251                     name = examineAliases(tznp, locale, requestID, alias, style, aliases);
 252                     if (name != null) {
 253                         return name;
 254                     }
 255                 }
 256             }
 257             return null;
 258         }
 259 
 260         private static String getName(TimeZoneNameProvider timeZoneNameProvider,
 261                                       Locale locale, String requestID, int style, String tzid) {
 262             String value = null;
 263             switch (requestID) {
 264             case "std":
 265                 value = timeZoneNameProvider.getDisplayName(tzid, false, style, locale);
 266                 break;
 267             case "dst":
 268                 value = timeZoneNameProvider.getDisplayName(tzid, true, style, locale);
 269                 break;
 270             case "generic":
 271                 value = timeZoneNameProvider.getGenericDisplayName(tzid, style, locale);
 272                 break;
 273             }
 274             return value;
 275         }
 276     }
 277 
 278     // No instantiation
 279     private TimeZoneNameUtility() {
 280     }
 281 }