1 /*
   2  * Copyright (c) 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 package sun.util.cldr;
  27 
  28 import static sun.util.locale.provider.LocaleProviderAdapter.Type;
  29 
  30 import java.text.MessageFormat;
  31 import java.util.Arrays;
  32 import java.util.Locale;
  33 import java.util.Objects;
  34 import java.util.ResourceBundle;
  35 import java.util.Set;
  36 import java.util.TimeZone;
  37 import java.util.stream.Collectors;
  38 import sun.util.calendar.ZoneInfoFile;
  39 import sun.util.locale.provider.LocaleProviderAdapter;
  40 import sun.util.locale.provider.LocaleResources;
  41 import sun.util.locale.provider.TimeZoneNameProviderImpl;
  42 import sun.util.locale.provider.TimeZoneNameUtility;
  43 
  44 /**
  45  * Concrete implementation of the
  46  * {@link java.util.spi.TimeZoneNameProvider TimeZoneNameProvider} class
  47  * for the CLDR LocaleProviderAdapter.
  48  *
  49  * @author Naoto Sato
  50  */
  51 public class CLDRTimeZoneNameProviderImpl extends TimeZoneNameProviderImpl {
  52 
  53     private static final String NO_INHERITANCE_MARKER = "\u2205\u2205\u2205";
  54     private static class AVAILABLE_IDS {
  55         static final String[] INSTANCE =
  56             Arrays.stream(ZoneInfoFile.getZoneIds())
  57                 .sorted()
  58                 .collect(Collectors.toList())
  59                 .toArray(new String[0]);
  60     }
  61 
  62     // display name array indexes
  63     private static final int INDEX_TZID         = 0;
  64     private static final int INDEX_STD_LONG     = 1;
  65     private static final int INDEX_STD_SHORT    = 2;
  66     private static final int INDEX_DST_LONG     = 3;
  67     private static final int INDEX_DST_SHORT    = 4;
  68     private static final int INDEX_GEN_LONG     = 5;
  69     private static final int INDEX_GEN_SHORT    = 6;
  70 
  71     public CLDRTimeZoneNameProviderImpl(Type type, Set<String> langtags) {
  72         super(type, langtags);
  73     }
  74 
  75     @Override
  76     protected String[] getDisplayNameArray(String id, Locale locale) {
  77         String tzid = TimeZoneNameUtility.canonicalTZID(id).orElse(id);
  78         String[] namesSuper = super.getDisplayNameArray(tzid, locale);
  79 
  80         if (Objects.nonNull(namesSuper)) {
  81             // CLDR's resource bundle has an translated entry for this id.
  82             // Fix up names if needed, either missing or no-inheritance
  83             namesSuper[INDEX_TZID] = id;
  84 
  85             // Check if standard long name exists. If not, try to retrieve the name
  86             // from language only locale resources. E.g., "Europe/London"
  87             // for en-GB only contains DST names
  88             if (!exists(namesSuper, INDEX_STD_LONG) && !locale.getCountry().isEmpty()) {
  89                 String[] names =
  90                         getDisplayNameArray(id, Locale.forLanguageTag(locale.getLanguage()));
  91                 if (exists(names, INDEX_STD_LONG)) {
  92                     namesSuper[INDEX_STD_LONG] = names[INDEX_STD_LONG];
  93                 }
  94             }
  95 
  96             for(int i = INDEX_STD_LONG; i < namesSuper.length; i++) { // index 0 is the 'id' itself
  97                 switch (namesSuper[i]) {
  98                 case "":
  99                     // Fill in empty elements
 100                     deriveFallbackName(namesSuper, i, locale,
 101                             namesSuper[INDEX_DST_LONG].isEmpty());
 102                     break;
 103                 case NO_INHERITANCE_MARKER:
 104                     // CLDR's "no inheritance marker"
 105                     namesSuper[i] = toGMTFormat(id, i == INDEX_DST_LONG || i == INDEX_DST_SHORT,
 106                                                 i % 2 != 0, locale);
 107                     break;
 108                 default:
 109                     break;
 110                 }
 111             }
 112             return namesSuper;
 113         } else {
 114             // Derive the names for this id. Validate the id first.
 115             if (Arrays.binarySearch(AVAILABLE_IDS.INSTANCE, id) >= 0) {
 116                 String[] names = new String[INDEX_GEN_SHORT + 1];
 117                 names[INDEX_TZID] = id;
 118                 deriveFallbackNames(names, locale);
 119                 return names;
 120             }
 121         }
 122 
 123         return null;
 124     }
 125 
 126     @Override
 127     protected String[][] getZoneStrings(Locale locale) {
 128         // Use English for the ROOT locale
 129         locale = locale.equals(Locale.ROOT) ? Locale.ENGLISH : locale;
 130         String[][] ret = super.getZoneStrings(locale);
 131 
 132         // Fill in for the empty names.
 133         // English names are prefilled for performance.
 134         if (locale.getLanguage() != "en") {
 135             for (int zoneIndex = 0; zoneIndex < ret.length; zoneIndex++) {
 136                 deriveFallbackNames(ret[zoneIndex], locale);
 137             }
 138         }
 139         return ret;
 140     }
 141 
 142     // Derive fallback time zone name according to LDML's logic
 143     private void deriveFallbackNames(String[] names, Locale locale) {
 144         for (int i = INDEX_STD_LONG; i <= INDEX_GEN_SHORT; i++) {
 145             deriveFallbackName(names, i, locale, false);
 146         }
 147     }
 148 
 149     private void deriveFallbackName(String[] names, int index, Locale locale, boolean noDST) {
 150         if (exists(names, index)) {
 151             return;
 152         }
 153 
 154         // Check if COMPAT can substitute the name
 155         if (LocaleProviderAdapter.getAdapterPreference().contains(Type.JRE)) {
 156             String[] compatNames = (String[])LocaleProviderAdapter.forJRE()
 157                 .getLocaleResources(locale)
 158                 .getTimeZoneNames(names[INDEX_TZID]);
 159             if (compatNames != null && exists(compatNames, index)) {
 160                 names[index] = compatNames[index];
 161                 return;
 162             }
 163         }
 164 
 165         // Type Fallback
 166         if (noDST && typeFallback(names, index)) {
 167             return;
 168         }
 169 
 170         // Region Fallback
 171         if (regionFormatFallback(names, index, locale)) {
 172             return;
 173         }
 174 
 175         // last resort
 176         String id = names[INDEX_TZID].toUpperCase(Locale.ROOT);
 177         if (!id.startsWith("ETC/GMT") &&
 178                 !id.startsWith("GMT") &&
 179                 !id.startsWith("UT")) {
 180             names[index] = toGMTFormat(names[INDEX_TZID],
 181                                        index == INDEX_DST_LONG || index == INDEX_DST_SHORT,
 182                                        index % 2 != 0,
 183                                        locale);
 184         }
 185     }
 186 
 187     private boolean exists(String[] names, int index) {
 188         return Objects.nonNull(names)
 189                 && Objects.nonNull(names[index])
 190                 && !names[index].isEmpty();
 191     }
 192 
 193     private boolean typeFallback(String[] names, int index) {
 194         // check generic
 195         int genIndex = INDEX_GEN_SHORT - index % 2;
 196         if (!exists(names, index) && exists(names, genIndex)) {
 197             names[index] = names[genIndex];
 198         } else {
 199             // check standard
 200             int stdIndex = INDEX_STD_SHORT - index % 2;
 201             if (!exists(names, index) && exists(names, stdIndex)) {
 202                 names[index] = names[stdIndex];
 203             }
 204         }
 205 
 206         return exists(names, index);
 207     }
 208 
 209     private boolean regionFormatFallback(String[] names, int index, Locale l) {
 210         String id = names[INDEX_TZID];
 211         LocaleResources lr = LocaleProviderAdapter.forType(Type.CLDR).getLocaleResources(l);
 212         ResourceBundle fd = lr.getJavaTimeFormatData();
 213 
 214         String rgn = (String) lr.getTimeZoneNames("timezone.excity." + id);
 215         if (rgn == null && !id.startsWith("Etc") && !id.startsWith("SystemV")) {
 216             int slash = id.lastIndexOf('/');
 217             if (slash > 0) {
 218                 rgn = id.substring(slash + 1).replaceAll("_", " ");
 219             }
 220         }
 221 
 222         if (rgn != null) {
 223             String fmt = "";
 224             switch (index) {
 225             case INDEX_STD_LONG:
 226                 fmt = fd.getString("timezone.regionFormat.standard");
 227                 break;
 228             case INDEX_DST_LONG:
 229                 fmt = fd.getString("timezone.regionFormat.daylight");
 230                 break;
 231             case INDEX_GEN_LONG:
 232                 fmt = fd.getString("timezone.regionFormat");
 233                 break;
 234             }
 235             if (!fmt.isEmpty()) {
 236                 names[index] = MessageFormat.format(fmt, rgn);
 237             }
 238         }
 239 
 240         return exists(names, index);
 241     }
 242 
 243     private String toGMTFormat(String id, boolean daylight, boolean isShort, Locale l) {
 244         TimeZone tz = ZoneInfoFile.getZoneInfo(id);
 245         int offset = (tz.getRawOffset() + (daylight ? tz.getDSTSavings() : 0)) / 60000;
 246         LocaleResources lr = LocaleProviderAdapter.forType(Type.CLDR).getLocaleResources(l);
 247         ResourceBundle fd = lr.getJavaTimeFormatData();
 248 
 249         if (offset == 0) {
 250             return fd.getString("timezone.gmtZeroFormat");
 251         } else {
 252             String gmtFormat = fd.getString("timezone.gmtFormat");
 253             String hourFormat = fd.getString("timezone.hourFormat");
 254 
 255             if (offset > 0) {
 256                 hourFormat = hourFormat.substring(0, hourFormat.indexOf(";"));
 257             } else {
 258                 hourFormat = hourFormat.substring(hourFormat.indexOf(";") + 1);
 259                 offset = -offset;
 260             }
 261             hourFormat = hourFormat
 262                 .replaceFirst("H+", (isShort ? "\\%1\\$d" : "\\%1\\$02d"))
 263                 .replaceFirst("m+", "\\%2\\$02d");
 264             return MessageFormat.format(gmtFormat,
 265                     String.format(hourFormat, offset / 60, offset % 60));
 266         }
 267     }
 268 }