1 /*
2  * Copyright (c) 2012, 2019, 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 android.icu.text.DateFormatSymbols;
29 import android.icu.util.ULocale;
30 
31 import static java.util.Calendar.*;
32 
33 import libcore.util.NonNull;
34 
35 import java.util.Calendar;
36 import java.util.LinkedHashMap;
37 import java.util.Locale;
38 import java.util.Map;
39 
40 // Android-changed: remove mention of CalendarDataProvider that's not used on Android.
41 /**
42  * {@code CalendarDataUtility} is a utility class for getting calendar field name values.
43  *
44  * @author Masayoshi Okutsu
45  * @author Naoto Sato
46  */
47 public class CalendarDataUtility {
48     // Android-note: This class has been rewritten from scratch and is effectively forked.
49     // The API (names of public constants, method signatures etc.) generally derives
50     // from OpenJDK so that other OpenJDK code that refers to this class doesn't need to
51     // be changed, but the implementation has been rewritten; logic / identifiers
52     // that weren't used from anywhere else have been dropped altogether.
53 
54     // Android-removed: Dead code, unused on Android.
55     // public static final String FIRST_DAY_OF_WEEK = "firstDayOfWeek";
56     // public static final String MINIMAL_DAYS_IN_FIRST_WEEK = "minimalDaysInFirstWeek";
57 
58     // Android-added: Calendar name constants for use in retrievFieldValueName.
59     private static final String ISLAMIC_CALENDAR = "islamic";
60     private static final String GREGORIAN_CALENDAR = "gregorian";
61     private static final String BUDDHIST_CALENDAR = "buddhist";
62     private static final String JAPANESE_CALENDAR = "japanese";
63 
64     // Android-added: REST_OF_STYLES array for use in retrieveFieldValueNames.
65     // ALL_STYLES implies SHORT_FORMAT and all of these values.
66     private static int[] REST_OF_STYLES = {
67             SHORT_STANDALONE, LONG_FORMAT, LONG_STANDALONE,
68             NARROW_FORMAT, NARROW_STANDALONE
69     };
70 
71     // No instantiation
CalendarDataUtility()72     private CalendarDataUtility() {
73     }
74 
75     // Android-changed: Modify retrieveFirstDayOfWeek() to provide the default value.
76     // public static int retrieveFirstDayOfWeek(Locale locale) {
77     /**
78      * @param defaultFirstDayOfWeek default first day of week if the {@code locale} doesn't have the
79      * "fw" extension. This default can be obtained from the locale via ICU4J
80      * {@code android.icu.util.Calendar}.
81      * @return
82      */
retrieveFirstDayOfWeek(Locale locale, int defaultFirstDayOfWeek)83     public static int retrieveFirstDayOfWeek(Locale locale, int defaultFirstDayOfWeek) {
84         // Look for the Unicode Extension in the locale parameter
85         if (locale.hasExtensions()) {
86             String fw = locale.getUnicodeLocaleType("fw");
87             if (fw != null) {
88                 switch (fw.toLowerCase(Locale.ROOT)) {
89                     case "mon":
90                         return MONDAY;
91                     case "tue":
92                         return TUESDAY;
93                     case "wed":
94                         return WEDNESDAY;
95                     case "thu":
96                         return THURSDAY;
97                     case "fri":
98                         return FRIDAY;
99                     case "sat":
100                         return SATURDAY;
101                     case "sun":
102                         return SUNDAY;
103                 }
104             }
105         }
106 
107         // Android-changed: Modify retrieveFirstDayOfWeek() to use the default first day of week.
108         /*
109         LocaleServiceProviderPool pool =
110                 LocaleServiceProviderPool.getPool(CalendarDataProvider.class);
111         Integer value = pool.getLocalizedObject(CalendarWeekParameterGetter.INSTANCE,
112                                                 findRegionOverride(locale),
113                                                 true, FIRST_DAY_OF_WEEK);
114         return (value != null && (value >= SUNDAY && value <= SATURDAY)) ? value : SUNDAY;
115         */
116         return defaultFirstDayOfWeek;
117     }
118 
119     // BEGIN Android-removed: Dead code, unused on Android.
120     /*
121     public static int retrieveMinimalDaysInFirstWeek(Locale locale) {
122         LocaleServiceProviderPool pool =
123                 LocaleServiceProviderPool.getPool(CalendarDataProvider.class);
124         Integer value = pool.getLocalizedObject(CalendarWeekParameterGetter.INSTANCE,
125                                                 findRegionOverride(locale),
126                                                 true, MINIMAL_DAYS_IN_FIRST_WEEK);
127         return (value != null && (value >= 1 && value <= 7)) ? value : 1;
128     }
129     */
130     // END Android-removed: Dead code, unused on Android.
131 
132     // BEGIN Android-changed: Implement on top of ICU.
133     /*
134     public static String retrieveFieldValueName(String id, int field, int value, int style, Locale locale) {
135         LocaleServiceProviderPool pool =
136                 LocaleServiceProviderPool.getPool(CalendarNameProvider.class);
137         return pool.getLocalizedObject(CalendarFieldValueNameGetter.INSTANCE, locale, normalizeCalendarType(id),
138                                        field, value, style, false);
139     }
140     */
retrieveFieldValueName(String id, int field, int value, int style, Locale locale)141     public static String retrieveFieldValueName(String id, int field, int value, int style,
142             Locale locale) {
143         if (field == Calendar.ERA) {
144             // For era the field value does not always equal the index into the names array.
145             switch (normalizeCalendarType(id)) {
146                 // These calendars have only one era, but represented it by the value 1.
147                 case BUDDHIST_CALENDAR:
148                 case ISLAMIC_CALENDAR:
149                     value -= 1;
150                     break;
151                 case JAPANESE_CALENDAR:
152                     // CLDR contains full data for historical eras, java.time only supports the 4
153                     // modern eras and numbers the modern eras starting with 1 (MEIJI). There are
154                     // 232 historical eras in CLDR/ICU so to get the real offset, we add 231.
155                     value += 231;
156                     break;
157                 default:
158                     // Other eras use 0-based values (e.g. 0=BCE, 1=CE for gregorian).
159                     break;
160             }
161         }
162         if (value < 0) {
163             return null;
164         }
165         String[] names = getNames(id, field, style, locale);
166         if (value >= names.length) {
167             return null;
168         }
169         return names[value];
170     }
171     // END Android-changed: Implement on top of ICU.
172 
173     // BEGIN Android-changed: Implement on top of ICU.
174     /*
175     public static String retrieveJavaTimeFieldValueName(String id, int field, int value, int style, Locale locale) {
176         LocaleServiceProviderPool pool =
177                 LocaleServiceProviderPool.getPool(CalendarNameProvider.class);
178         String name;
179         name = pool.getLocalizedObject(CalendarFieldValueNameGetter.INSTANCE, locale, normalizeCalendarType(id),
180                                        field, value, style, true);
181         if (name == null) {
182             name = pool.getLocalizedObject(CalendarFieldValueNameGetter.INSTANCE, locale, normalizeCalendarType(id),
183                                            field, value, style, false);
184         }
185         return name;
186     }
187     */
retrieveJavaTimeFieldValueName(String id, int field, int value, int style, Locale locale)188     public static String retrieveJavaTimeFieldValueName(String id, int field, int value, int style,
189             Locale locale) {
190         // Don't distinguish between retrieve* and retrieveJavaTime* methods.
191         return retrieveFieldValueName(id, field, value, style, locale);
192     }
193     // END Android-changed: Implement on top of ICU.
194 
195     // BEGIN Android-changed: Implement on top of ICU.
196     /*
197     public static Map<String, Integer> retrieveFieldValueNames(String id, int field, int style, Locale locale) {
198         LocaleServiceProviderPool pool =
199             LocaleServiceProviderPool.getPool(CalendarNameProvider.class);
200         return pool.getLocalizedObject(CalendarFieldValueNamesMapGetter.INSTANCE, locale,
201                                        normalizeCalendarType(id), field, style, false);
202     }
203     */
retrieveFieldValueNames(String id, int field, int style, Locale locale)204     public static Map<String, Integer> retrieveFieldValueNames(String id, int field, int style,
205             Locale locale) {
206         Map<String, Integer> names;
207         if (style == ALL_STYLES) {
208             names = retrieveFieldValueNamesImpl(id, field, SHORT_FORMAT, locale);
209             for (int st : REST_OF_STYLES) {
210                 names.putAll(retrieveFieldValueNamesImpl(id, field, st, locale));
211             }
212         } else {
213             // specific style
214             names = retrieveFieldValueNamesImpl(id, field, style, locale);
215         }
216         return names.isEmpty() ? null : names;
217     }
218     // END Android-changed: Implement on top of ICU.
219 
220     // BEGIN Android-changed: Implement on top of ICU.
221     /*
222     public static Map<String, Integer> retrieveJavaTimeFieldValueNames(String id, int field, int style, Locale locale) {
223         LocaleServiceProviderPool pool =
224             LocaleServiceProviderPool.getPool(CalendarNameProvider.class);
225         Map<String, Integer> map;
226         map = pool.getLocalizedObject(CalendarFieldValueNamesMapGetter.INSTANCE, locale,
227                                        normalizeCalendarType(id), field, style, true);
228         if (map == null) {
229             map = pool.getLocalizedObject(CalendarFieldValueNamesMapGetter.INSTANCE, locale,
230                                            normalizeCalendarType(id), field, style, false);
231         }
232         return map;
233     }
234     */
retrieveJavaTimeFieldValueNames(String id, int field, int style, Locale locale)235     public static Map<String, Integer> retrieveJavaTimeFieldValueNames(String id, int field,
236             int style, Locale locale) {
237         // Don't distinguish between retrieve* and retrieveJavaTime* methods.
238         return retrieveFieldValueNames(id, field, style, locale);
239     }
240     // END Android-changed: Implement on top of ICU.
241 
242     /**
243      * Utility to look for a region override extension.
244      * If no region override is found, returns the original locale.
245      */
246     // BEGIN Android-removed: Dead code, unused on Android.
247     /*
248     public static Locale findRegionOverride(Locale l) {
249         String rg = l.getUnicodeLocaleType("rg");
250         Locale override = l;
251 
252         if (rg != null && rg.length() == 6) {
253             // UN M.49 code should not be allowed here
254             // cannot use regex here, as it could be a recursive call
255             rg = rg.toUpperCase(Locale.ROOT);
256             if (rg.charAt(0) >= 0x0041 &&
257                 rg.charAt(0) <= 0x005A &&
258                 rg.charAt(1) >= 0x0041 &&
259                 rg.charAt(1) <= 0x005A &&
260                 rg.substring(2).equals("ZZZZ")) {
261                 override = new Locale.Builder().setLocale(l)
262                     .setRegion(rg.substring(0, 2))
263                     .build();
264             }
265         }
266 
267         return override;
268     }
269     */
270     // END Android-removed: Dead code, unused on Android.
271 
272     // Android-changed: Added private modifier for normalizeCalendarType().
273     // static String normalizeCalendarType(String requestID) {
normalizeCalendarType(String requestID)274     private static String normalizeCalendarType(String requestID) {
275         String type;
276         // Android-changed: normalize "gregory" to "gregorian", not the other way around.
277         // Android maps BCP-47 calendar types to LDML defined calendar types, because it uses
278         // ICU directly while the upstream does the opposite because the upstream uses different
279         // data sources. See android.icu.text.DateFormatSymbols.CALENDAR_CLASSES for reference.
280         // if (requestID.equals("gregorian") || requestID.equals("iso8601")) {
281         //    type = "gregory";
282         // } else if (requestID.startsWith("islamic")) {
283         //    type = "islamic";
284         if (requestID.equals("gregory") || requestID.equals("iso8601")) {
285             type = GREGORIAN_CALENDAR;
286         } else if (requestID.startsWith(ISLAMIC_CALENDAR)) {
287             type = ISLAMIC_CALENDAR;
288         } else {
289             type = requestID;
290         }
291         return type;
292     }
293 
294     // BEGIN Android-added: Various private helper methods.
retrieveFieldValueNamesImpl(String id, int field, int style, Locale locale)295     private static Map<String, Integer> retrieveFieldValueNamesImpl(String id, int field, int style,
296             Locale locale) {
297         String[] names = getNames(id, field, style, locale);
298         int skipped = 0;
299         int offset = 0;
300         if (field == Calendar.ERA) {
301             // See retrieveFieldValueName() for explanation of this code and the values used.
302             switch (normalizeCalendarType(id)) {
303                 case BUDDHIST_CALENDAR:
304                 case ISLAMIC_CALENDAR:
305                     offset = 1;
306                     break;
307                 case JAPANESE_CALENDAR:
308                     skipped = 232;
309                     offset = -231;
310                     break;
311                 default:
312                     break;
313             }
314         }
315         Map<String, Integer> result = new LinkedHashMap<>();
316         for (int i = skipped; i < names.length; i++) {
317             if (names[i].isEmpty()) {
318                 continue;
319             }
320 
321             if (result.put(names[i], i + offset) != null) {
322                 // Duplicate names indicate that the names would be ambiguous. Skip this style for
323                 // ALL_STYLES. In other cases this results in null being returned in
324                 // retrieveValueNames(), which is required by Calendar.getDisplayNames().
325                 return new LinkedHashMap<>();
326             }
327         }
328         return result;
329     }
330 
getNames(String id, int field, int style, Locale locale)331     private static String[] getNames(String id, int field, int style, Locale locale) {
332         int context = toContext(style);
333         int width = toWidth(style);
334         DateFormatSymbols symbols = getDateFormatSymbols(id, locale);
335         switch (field) {
336             case Calendar.MONTH:
337                 return symbols.getMonths(context, width);
338             case Calendar.ERA:
339                 switch (width) {
340                     case DateFormatSymbols.NARROW:
341                         return symbols.getNarrowEras();
342                     case DateFormatSymbols.ABBREVIATED:
343                         return symbols.getEras();
344                     case DateFormatSymbols.WIDE:
345                         return symbols.getEraNames();
346                     default:
347                         throw new UnsupportedOperationException("Unknown width: " + width);
348                 }
349             case Calendar.DAY_OF_WEEK:
350                 return symbols.getWeekdays(context, width);
351             case Calendar.AM_PM:
352                 return symbols.getAmPmStrings();
353             default:
354                 throw new UnsupportedOperationException("Unknown field: " + field);
355         }
356     }
357 
getDateFormatSymbols(@onNull String id, Locale locale)358     private static DateFormatSymbols getDateFormatSymbols(@NonNull String id, Locale locale) {
359         String calendarType = normalizeCalendarType(id);
360         ULocale uLocale = ULocale.forLocale(locale)
361                 .setKeywordValue("calendar", calendarType);
362         return new DateFormatSymbols(uLocale);
363     }
364 
365     /**
366      * Transform a {@link Calendar} style constant into an ICU width value.
367      */
toWidth(int style)368     private static int toWidth(int style) {
369         switch (style) {
370             case Calendar.SHORT_FORMAT:
371             case Calendar.SHORT_STANDALONE:
372                 return DateFormatSymbols.ABBREVIATED;
373             case Calendar.NARROW_FORMAT:
374             case Calendar.NARROW_STANDALONE:
375                 return DateFormatSymbols.NARROW;
376             case Calendar.LONG_FORMAT:
377             case Calendar.LONG_STANDALONE:
378                 return DateFormatSymbols.WIDE;
379             default:
380                 throw new IllegalArgumentException("Invalid style: " + style);
381         }
382     }
383 
384     /**
385      * Transform a {@link Calendar} style constant into an ICU context value.
386      */
toContext(int style)387     private static int toContext(int style) {
388         switch (style) {
389             case Calendar.SHORT_FORMAT:
390             case Calendar.NARROW_FORMAT:
391             case Calendar.LONG_FORMAT:
392                 return DateFormatSymbols.FORMAT;
393             case Calendar.SHORT_STANDALONE:
394             case Calendar.NARROW_STANDALONE:
395             case Calendar.LONG_STANDALONE:
396                 return DateFormatSymbols.STANDALONE;
397             default:
398                 throw new IllegalArgumentException("Invalid style: " + style);
399         }
400     }
401     // END Android-added: Various private helper methods.
402 
403     // BEGIN Android-removed: Dead code, unused on Android.
404     /*
405     /**
406      * Obtains a localized field value string from a CalendarDataProvider
407      * implementation.
408      *
409     private static class CalendarFieldValueNameGetter
410         implements LocaleServiceProviderPool.LocalizedObjectGetter<CalendarNameProvider,
411                                                                    String> {
412         private static final CalendarFieldValueNameGetter INSTANCE =
413             new CalendarFieldValueNameGetter();
414 
415         @Override
416         public String getObject(CalendarNameProvider calendarNameProvider,
417                                 Locale locale,
418                                 String requestID, // calendarType
419                                 Object... params) {
420             assert params.length == 4;
421             int field = (int) params[0];
422             int value = (int) params[1];
423             int style = (int) params[2];
424             boolean javatime = (boolean) params[3];
425 
426             // If javatime is true, resources from CLDR have precedence over JRE
427             // native resources.
428             if (javatime && calendarNameProvider instanceof CalendarNameProviderImpl) {
429                 String name;
430                 name = ((CalendarNameProviderImpl)calendarNameProvider)
431                         .getJavaTimeDisplayName(requestID, field, value, style, locale);
432                 return name;
433             }
434             return calendarNameProvider.getDisplayName(requestID, field, value, style, locale);
435         }
436     }
437 
438     /**
439      * Obtains a localized field-value pairs from a CalendarDataProvider
440      * implementation.
441      *
442     private static class CalendarFieldValueNamesMapGetter
443         implements LocaleServiceProviderPool.LocalizedObjectGetter<CalendarNameProvider,
444                                                                    Map<String, Integer>> {
445         private static final CalendarFieldValueNamesMapGetter INSTANCE =
446             new CalendarFieldValueNamesMapGetter();
447 
448         @Override
449         public Map<String, Integer> getObject(CalendarNameProvider calendarNameProvider,
450                                               Locale locale,
451                                               String requestID, // calendarType
452                                               Object... params) {
453             assert params.length == 3;
454             int field = (int) params[0];
455             int style = (int) params[1];
456             boolean javatime = (boolean) params[2];
457 
458             // If javatime is true, resources from CLDR have precedence over JRE
459             // native resources.
460             if (javatime && calendarNameProvider instanceof CalendarNameProviderImpl) {
461                 Map<String, Integer> map;
462                 map = ((CalendarNameProviderImpl)calendarNameProvider)
463                         .getJavaTimeDisplayNames(requestID, field, style, locale);
464                 return map;
465             }
466             return calendarNameProvider.getDisplayNames(requestID, field, style, locale);
467         }
468     }
469 
470     private static class CalendarWeekParameterGetter
471         implements LocaleServiceProviderPool.LocalizedObjectGetter<CalendarDataProvider,
472                                                                    Integer> {
473         private static final CalendarWeekParameterGetter INSTANCE =
474             new CalendarWeekParameterGetter();
475 
476         @Override
477         public Integer getObject(CalendarDataProvider calendarDataProvider,
478                                  Locale locale,
479                                  String requestID,    // resource key
480                                  Object... params) {
481             assert params.length == 0;
482             int value;
483             switch (requestID) {
484             case FIRST_DAY_OF_WEEK:
485                 value = calendarDataProvider.getFirstDayOfWeek(locale);
486                 if (value == 0) {
487                     value = MONDAY; // default for the world ("001")
488                 }
489                 break;
490             case MINIMAL_DAYS_IN_FIRST_WEEK:
491                 value = calendarDataProvider.getMinimalDaysInFirstWeek(locale);
492                 if (value == 0) {
493                     value = 1; // default for the world ("001")
494                 }
495                 break;
496             default:
497                 throw new InternalError("invalid requestID: " + requestID);
498             }
499 
500             assert value != 0;
501             return value;
502         }
503     }
504     */
505     // END Android-removed: Dead code, unused on Android.
506 }
507