1 /*
2  * Copyright (c) 2018, 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 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 sun.util.calendar.ZoneInfoFile;
38 import sun.util.locale.provider.LocaleProviderAdapter;
39 import sun.util.locale.provider.LocaleResources;
40 import sun.util.locale.provider.TimeZoneNameProviderImpl;
41 import sun.util.locale.provider.TimeZoneNameUtility;
42 
43 /**
44  * Concrete implementation of the
45  * {@link java.util.spi.TimeZoneNameProvider TimeZoneNameProvider} class
46  * for the CLDR LocaleProviderAdapter.
47  *
48  * @author Naoto Sato
49  */
50 public class CLDRTimeZoneNameProviderImpl extends TimeZoneNameProviderImpl {
51 
52     private static final String NO_INHERITANCE_MARKER = "\u2205\u2205\u2205";
53     private static class AVAILABLE_IDS {
54         static final String[] INSTANCE =
55             Arrays.stream(ZoneInfoFile.getZoneIds())
56                 .sorted()
57                 .toArray(String[]::new);
58     }
59 
60     // display name array indexes
61     private static final int INDEX_TZID         = 0;
62     private static final int INDEX_STD_LONG     = 1;
63     private static final int INDEX_STD_SHORT    = 2;
64     private static final int INDEX_DST_LONG     = 3;
65     private static final int INDEX_DST_SHORT    = 4;
66     private static final int INDEX_GEN_LONG     = 5;
67     private static final int INDEX_GEN_SHORT    = 6;
68 
CLDRTimeZoneNameProviderImpl(Type type, Set<String> langtags)69     public CLDRTimeZoneNameProviderImpl(Type type, Set<String> langtags) {
70         super(type, langtags);
71     }
72 
73     @Override
getDisplayNameArray(String id, Locale locale)74     protected String[] getDisplayNameArray(String id, Locale locale) {
75         String[] namesSuper = super.getDisplayNameArray(id, locale);
76 
77         if (namesSuper == null) {
78             // try canonical id instead
79             namesSuper = super.getDisplayNameArray(
80                 TimeZoneNameUtility.canonicalTZID(id).orElse(id),
81                 locale);
82         }
83 
84         if (namesSuper != null) {
85             // CLDR's resource bundle has an translated entry for this id.
86             // Fix up names if needed, either missing or no-inheritance
87             namesSuper[INDEX_TZID] = id;
88 
89             for(int i = INDEX_STD_LONG; i < namesSuper.length; i++) { // index 0 is the 'id' itself
90                 switch (namesSuper[i]) {
91                 case "":
92                     // Fill in empty elements
93                     deriveFallbackName(namesSuper, i, locale,
94                                        !TimeZone.getTimeZone(id).useDaylightTime());
95                     break;
96                 case NO_INHERITANCE_MARKER:
97                     // CLDR's "no inheritance marker"
98                     namesSuper[i] = toGMTFormat(id, i == INDEX_DST_LONG || i == INDEX_DST_SHORT,
99                                                 locale);
100                     break;
101                 default:
102                     break;
103                 }
104             }
105             return namesSuper;
106         } else {
107             // Derive the names for this id. Validate the id first.
108             if (Arrays.binarySearch(AVAILABLE_IDS.INSTANCE, id) >= 0) {
109                 String[] names = new String[INDEX_GEN_SHORT + 1];
110                 names[INDEX_TZID] = id;
111                 deriveFallbackNames(names, locale);
112                 return names;
113             }
114         }
115 
116         return null;
117     }
118 
119     @Override
getZoneStrings(Locale locale)120     protected String[][] getZoneStrings(Locale locale) {
121         String[][] ret = super.getZoneStrings(locale);
122 
123         // Fill in for the empty names.
124         for (int zoneIndex = 0; zoneIndex < ret.length; zoneIndex++) {
125             deriveFallbackNames(ret[zoneIndex], locale);
126         }
127         return ret;
128     }
129 
130     // Derive fallback time zone name according to LDML's logic
deriveFallbackNames(String[] names, Locale locale)131     private void deriveFallbackNames(String[] names, Locale locale) {
132         boolean noDST = !TimeZone.getTimeZone(names[0]).useDaylightTime();
133 
134         for (int i = INDEX_STD_LONG; i <= INDEX_GEN_SHORT; i++) {
135             deriveFallbackName(names, i, locale, noDST);
136         }
137     }
138 
deriveFallbackName(String[] names, int index, Locale locale, boolean noDST)139     private void deriveFallbackName(String[] names, int index, Locale locale, boolean noDST) {
140         String id = names[INDEX_TZID];
141 
142         if (exists(names, index)) {
143             if (names[index].equals(NO_INHERITANCE_MARKER)) {
144                 // CLDR's "no inheritance marker"
145                 names[index] = toGMTFormat(id,
146                                     index == INDEX_DST_LONG || index == INDEX_DST_SHORT,
147                                     locale);
148             }
149             return;
150         }
151 
152         // Check parent locale first
153         if (!exists(names, index)) {
154             CLDRLocaleProviderAdapter clpa = (CLDRLocaleProviderAdapter)LocaleProviderAdapter.forType(Type.CLDR);
155             var cands = clpa.getCandidateLocales("", locale);
156             if (cands.size() > 1) {
157                 var parentLoc = cands.get(1); // immediate parent locale
158                 String[] parentNames = super.getDisplayNameArray(id, parentLoc);
159                 if (parentNames != null && !parentNames[index].isEmpty()) {
160                     names[index] = parentNames[index];
161                     return;
162                 }
163             }
164         }
165 
166         // Check if COMPAT can substitute the name
167         if (LocaleProviderAdapter.getAdapterPreference().contains(Type.JRE)) {
168             String[] compatNames = (String[])LocaleProviderAdapter.forJRE()
169                 .getLocaleResources(mapChineseLocale(locale))
170                 .getTimeZoneNames(id);
171             if (compatNames != null) {
172                 for (int i = INDEX_STD_LONG; i <= INDEX_GEN_SHORT; i++) {
173                     // Assumes COMPAT has no empty slots
174                     if (i == index || !exists(names, i)) {
175                         names[i] = compatNames[i];
176                     }
177                 }
178                 return;
179             }
180         }
181 
182         // Region Fallback
183         if (regionFormatFallback(names, index, locale)) {
184             return;
185         }
186 
187         // Type Fallback
188         if (noDST && typeFallback(names, index)) {
189             return;
190         }
191 
192         // last resort
193         names[index] = toGMTFormat(id,
194                                    index == INDEX_DST_LONG || index == INDEX_DST_SHORT,
195                                    locale);
196         // aliases of "GMT" timezone.
197         if ((exists(names, INDEX_STD_LONG)) && (id.startsWith("Etc/")
198                 || id.startsWith("GMT") || id.startsWith("Greenwich"))) {
199             switch (id) {
200             case "Etc/GMT":
201             case "Etc/GMT-0":
202             case "Etc/GMT+0":
203             case "Etc/GMT0":
204             case "GMT+0":
205             case "GMT-0":
206             case "GMT0":
207             case "Greenwich":
208                 names[INDEX_DST_LONG] = names[INDEX_GEN_LONG] = names[INDEX_STD_LONG];
209                 break;
210             }
211         }
212     }
213 
exists(String[] names, int index)214     private boolean exists(String[] names, int index) {
215         return Objects.nonNull(names)
216                 && Objects.nonNull(names[index])
217                 && !names[index].isEmpty();
218     }
219 
typeFallback(String[] names, int index)220     private boolean typeFallback(String[] names, int index) {
221         // check generic
222         int genIndex = INDEX_GEN_SHORT - index % 2;
223         if (!exists(names, index) && exists(names, genIndex) && !names[genIndex].startsWith("GMT")) {
224             names[index] = names[genIndex];
225         } else {
226             // check standard
227             int stdIndex = INDEX_STD_SHORT - index % 2;
228             if (!exists(names, index) && exists(names, stdIndex) && !names[stdIndex].startsWith("GMT")) {
229                 names[index] = names[stdIndex];
230             }
231         }
232 
233         return exists(names, index);
234     }
235 
regionFormatFallback(String[] names, int index, Locale l)236     private boolean regionFormatFallback(String[] names, int index, Locale l) {
237         String id = names[INDEX_TZID];
238         LocaleResources lr = LocaleProviderAdapter.forType(Type.CLDR).getLocaleResources(l);
239         ResourceBundle fd = lr.getJavaTimeFormatData();
240 
241         id = TimeZoneNameUtility.canonicalTZID(id).orElse(id);
242         String rgn = (String) lr.getTimeZoneNames("timezone.excity." + id);
243         if (rgn == null && !id.startsWith("Etc") && !id.startsWith("SystemV")) {
244             int slash = id.lastIndexOf('/');
245             if (slash > 0) {
246                 rgn = id.substring(slash + 1).replaceAll("_", " ");
247             }
248         }
249 
250         if (rgn != null) {
251             String fmt = "";
252             switch (index) {
253             case INDEX_STD_LONG:
254                 fmt = fd.getString("timezone.regionFormat.standard");
255                 break;
256             case INDEX_DST_LONG:
257                 fmt = fd.getString("timezone.regionFormat.daylight");
258                 break;
259             case INDEX_GEN_LONG:
260                 fmt = fd.getString("timezone.regionFormat");
261                 break;
262             }
263             if (!fmt.isEmpty()) {
264                 names[index] = MessageFormat.format(fmt, rgn);
265             }
266         }
267 
268         return exists(names, index);
269     }
270 
toGMTFormat(String id, boolean daylight, Locale l)271     private String toGMTFormat(String id, boolean daylight, Locale l) {
272         TimeZone tz = ZoneInfoFile.getZoneInfo(id);
273         int offset = (tz.getRawOffset() + (daylight ? tz.getDSTSavings() : 0)) / 60000;
274         LocaleResources lr = LocaleProviderAdapter.forType(Type.CLDR).getLocaleResources(l);
275         ResourceBundle fd = lr.getJavaTimeFormatData();
276 
277         if (offset == 0) {
278             return fd.getString("timezone.gmtZeroFormat");
279         } else {
280             String gmtFormat = fd.getString("timezone.gmtFormat");
281             String hourFormat = fd.getString("timezone.hourFormat");
282 
283             if (offset > 0) {
284                 hourFormat = hourFormat.substring(0, hourFormat.indexOf(";"));
285             } else {
286                 hourFormat = hourFormat.substring(hourFormat.indexOf(";") + 1);
287                 offset = -offset;
288             }
289             hourFormat = hourFormat
290                 .replaceFirst("H+", "\\%1\\$02d")
291                 .replaceFirst("m+", "\\%2\\$02d");
292             return MessageFormat.format(gmtFormat,
293                     String.format(l, hourFormat, offset / 60, offset % 60));
294         }
295     }
296 
297     // Mapping CLDR's Simplified/Traditional Chinese resources
298     // to COMPAT's zh-CN/TW
mapChineseLocale(Locale locale)299     private Locale mapChineseLocale(Locale locale) {
300         if (locale.getLanguage() == "zh") {
301             switch (locale.getScript()) {
302                 case "Hans":
303                     return Locale.CHINA;
304                 case "Hant":
305                     return Locale.TAIWAN;
306                 case "":
307                     // no script, guess from country code.
308                     switch (locale.getCountry()) {
309                         case "":
310                         case "CN":
311                         case "SG":
312                             return Locale.CHINA;
313                         case "HK":
314                         case "MO":
315                         case "TW":
316                             return Locale.TAIWAN;
317                     }
318                     break;
319             }
320         }
321 
322         // no need to map
323         return locale;
324     }
325 }
326