1 /*
2  * Copyright (c) 2012, 2013, 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  * This file is available under and governed by the GNU General Public
28  * License version 2 only, as published by the Free Software Foundation.
29  * However, the following notice accompanied the original version of this
30  * file:
31  *
32  * Copyright (c) 2011-2012, Stephen Colebourne & Michael Nascimento Santos
33  *
34  * All rights reserved.
35  *
36  * Redistribution and use in source and binary forms, with or without
37  * modification, are permitted provided that the following conditions are met:
38  *
39  *  * Redistributions of source code must retain the above copyright notice,
40  *    this list of conditions and the following disclaimer.
41  *
42  *  * Redistributions in binary form must reproduce the above copyright notice,
43  *    this list of conditions and the following disclaimer in the documentation
44  *    and/or other materials provided with the distribution.
45  *
46  *  * Neither the name of JSR-310 nor the names of its contributors
47  *    may be used to endorse or promote products derived from this software
48  *    without specific prior written permission.
49  *
50  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
51  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
52  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
53  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
54  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
55  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
56  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
57  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
58  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
59  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
60  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
61  */
62 package java.time.format;
63 
64 import android.icu.impl.ICUData;
65 import android.icu.impl.ICUResourceBundle;
66 import android.icu.util.UResourceBundle;
67 
68 import static java.time.temporal.ChronoField.AMPM_OF_DAY;
69 import static java.time.temporal.ChronoField.DAY_OF_WEEK;
70 import static java.time.temporal.ChronoField.ERA;
71 import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
72 
73 import java.time.chrono.Chronology;
74 import java.time.chrono.IsoChronology;
75 import java.time.chrono.JapaneseChronology;
76 import java.time.temporal.ChronoField;
77 import java.time.temporal.IsoFields;
78 import java.time.temporal.TemporalField;
79 import java.util.AbstractMap.SimpleImmutableEntry;
80 import java.util.ArrayList;
81 import java.util.Calendar;
82 import java.util.Collections;
83 import java.util.Comparator;
84 import java.util.HashMap;
85 import java.util.Iterator;
86 import java.util.List;
87 import java.util.Locale;
88 import java.util.Map;
89 import java.util.Map.Entry;
90 import java.util.concurrent.ConcurrentHashMap;
91 import java.util.concurrent.ConcurrentMap;
92 
93 import sun.util.locale.provider.CalendarDataUtility;
94 
95 /**
96  * A provider to obtain the textual form of a date-time field.
97  *
98  * @implSpec
99  * Implementations must be thread-safe.
100  * Implementations should cache the textual information.
101  *
102  * @since 1.8
103  */
104 class DateTimeTextProvider {
105 
106     /** Cache. */
107     private static final ConcurrentMap<Entry<TemporalField, Locale>, Object> CACHE = new ConcurrentHashMap<>(16, 0.75f, 2);
108     /** Comparator. */
109     private static final Comparator<Entry<String, Long>> COMPARATOR = new Comparator<Entry<String, Long>>() {
110         @Override
111         public int compare(Entry<String, Long> obj1, Entry<String, Long> obj2) {
112             return obj2.getKey().length() - obj1.getKey().length();  // longest to shortest
113         }
114     };
115 
DateTimeTextProvider()116     DateTimeTextProvider() {}
117 
118     /**
119      * Gets the provider of text.
120      *
121      * @return the provider, not null
122      */
getInstance()123     static DateTimeTextProvider getInstance() {
124         return new DateTimeTextProvider();
125     }
126 
127     /**
128      * Gets the text for the specified field, locale and style
129      * for the purpose of formatting.
130      * <p>
131      * The text associated with the value is returned.
132      * The null return value should be used if there is no applicable text, or
133      * if the text would be a numeric representation of the value.
134      *
135      * @param field  the field to get text for, not null
136      * @param value  the field value to get text for, not null
137      * @param style  the style to get text for, not null
138      * @param locale  the locale to get text for, not null
139      * @return the text for the field value, null if no text found
140      */
getText(TemporalField field, long value, TextStyle style, Locale locale)141     public String getText(TemporalField field, long value, TextStyle style, Locale locale) {
142         Object store = findStore(field, locale);
143         if (store instanceof LocaleStore) {
144             return ((LocaleStore) store).getText(value, style);
145         }
146         return null;
147     }
148 
149     /**
150      * Gets the text for the specified chrono, field, locale and style
151      * for the purpose of formatting.
152      * <p>
153      * The text associated with the value is returned.
154      * The null return value should be used if there is no applicable text, or
155      * if the text would be a numeric representation of the value.
156      *
157      * @param chrono  the Chronology to get text for, not null
158      * @param field  the field to get text for, not null
159      * @param value  the field value to get text for, not null
160      * @param style  the style to get text for, not null
161      * @param locale  the locale to get text for, not null
162      * @return the text for the field value, null if no text found
163      */
getText(Chronology chrono, TemporalField field, long value, TextStyle style, Locale locale)164     public String getText(Chronology chrono, TemporalField field, long value,
165                                     TextStyle style, Locale locale) {
166         if (chrono == IsoChronology.INSTANCE
167                 || !(field instanceof ChronoField)) {
168             return getText(field, value, style, locale);
169         }
170 
171         int fieldIndex;
172         int fieldValue;
173         if (field == ERA) {
174             fieldIndex = Calendar.ERA;
175             if (chrono == JapaneseChronology.INSTANCE) {
176                 if (value == -999) {
177                     fieldValue = 0;
178                 } else {
179                     fieldValue = (int) value + 2;
180                 }
181             } else {
182                 fieldValue = (int) value;
183             }
184         } else if (field == MONTH_OF_YEAR) {
185             fieldIndex = Calendar.MONTH;
186             fieldValue = (int) value - 1;
187         } else if (field == DAY_OF_WEEK) {
188             fieldIndex = Calendar.DAY_OF_WEEK;
189             fieldValue = (int) value + 1;
190             if (fieldValue > 7) {
191                 fieldValue = Calendar.SUNDAY;
192             }
193         } else if (field == AMPM_OF_DAY) {
194             fieldIndex = Calendar.AM_PM;
195             fieldValue = (int) value;
196         } else {
197             return null;
198         }
199         return CalendarDataUtility.retrieveJavaTimeFieldValueName(
200                 chrono.getCalendarType(), fieldIndex, fieldValue, style.toCalendarStyle(), locale);
201     }
202 
203     /**
204      * Gets an iterator of text to field for the specified field, locale and style
205      * for the purpose of parsing.
206      * <p>
207      * The iterator must be returned in order from the longest text to the shortest.
208      * <p>
209      * The null return value should be used if there is no applicable parsable text, or
210      * if the text would be a numeric representation of the value.
211      * Text can only be parsed if all the values for that field-style-locale combination are unique.
212      *
213      * @param field  the field to get text for, not null
214      * @param style  the style to get text for, null for all parsable text
215      * @param locale  the locale to get text for, not null
216      * @return the iterator of text to field pairs, in order from longest text to shortest text,
217      *  null if the field or style is not parsable
218      */
getTextIterator(TemporalField field, TextStyle style, Locale locale)219     public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) {
220         Object store = findStore(field, locale);
221         if (store instanceof LocaleStore) {
222             return ((LocaleStore) store).getTextIterator(style);
223         }
224         return null;
225     }
226 
227     /**
228      * Gets an iterator of text to field for the specified chrono, field, locale and style
229      * for the purpose of parsing.
230      * <p>
231      * The iterator must be returned in order from the longest text to the shortest.
232      * <p>
233      * The null return value should be used if there is no applicable parsable text, or
234      * if the text would be a numeric representation of the value.
235      * Text can only be parsed if all the values for that field-style-locale combination are unique.
236      *
237      * @param chrono  the Chronology to get text for, not null
238      * @param field  the field to get text for, not null
239      * @param style  the style to get text for, null for all parsable text
240      * @param locale  the locale to get text for, not null
241      * @return the iterator of text to field pairs, in order from longest text to shortest text,
242      *  null if the field or style is not parsable
243      */
getTextIterator(Chronology chrono, TemporalField field, TextStyle style, Locale locale)244     public Iterator<Entry<String, Long>> getTextIterator(Chronology chrono, TemporalField field,
245                                                          TextStyle style, Locale locale) {
246         if (chrono == IsoChronology.INSTANCE
247                 || !(field instanceof ChronoField)) {
248             return getTextIterator(field, style, locale);
249         }
250 
251         int fieldIndex;
252         switch ((ChronoField)field) {
253         case ERA:
254             fieldIndex = Calendar.ERA;
255             break;
256         case MONTH_OF_YEAR:
257             fieldIndex = Calendar.MONTH;
258             break;
259         case DAY_OF_WEEK:
260             fieldIndex = Calendar.DAY_OF_WEEK;
261             break;
262         case AMPM_OF_DAY:
263             fieldIndex = Calendar.AM_PM;
264             break;
265         default:
266             return null;
267         }
268 
269         int calendarStyle = (style == null) ? Calendar.ALL_STYLES : style.toCalendarStyle();
270         Map<String, Integer> map = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
271                 chrono.getCalendarType(), fieldIndex, calendarStyle, locale);
272         if (map == null) {
273             return null;
274         }
275         List<Entry<String, Long>> list = new ArrayList<>(map.size());
276         switch (fieldIndex) {
277         case Calendar.ERA:
278             for (Map.Entry<String, Integer> entry : map.entrySet()) {
279                 int era = entry.getValue();
280                 if (chrono == JapaneseChronology.INSTANCE) {
281                     if (era == 0) {
282                         era = -999;
283                     } else {
284                         era -= 2;
285                     }
286                 }
287                 list.add(createEntry(entry.getKey(), (long)era));
288             }
289             break;
290         case Calendar.MONTH:
291             for (Map.Entry<String, Integer> entry : map.entrySet()) {
292                 list.add(createEntry(entry.getKey(), (long)(entry.getValue() + 1)));
293             }
294             break;
295         case Calendar.DAY_OF_WEEK:
296             for (Map.Entry<String, Integer> entry : map.entrySet()) {
297                 list.add(createEntry(entry.getKey(), (long)toWeekDay(entry.getValue())));
298             }
299             break;
300         default:
301             for (Map.Entry<String, Integer> entry : map.entrySet()) {
302                 list.add(createEntry(entry.getKey(), (long)entry.getValue()));
303             }
304             break;
305         }
306         return list.iterator();
307     }
308 
findStore(TemporalField field, Locale locale)309     private Object findStore(TemporalField field, Locale locale) {
310         Entry<TemporalField, Locale> key = createEntry(field, locale);
311         Object store = CACHE.get(key);
312         if (store == null) {
313             store = createStore(field, locale);
314             CACHE.putIfAbsent(key, store);
315             store = CACHE.get(key);
316         }
317         return store;
318     }
319 
toWeekDay(int calWeekDay)320     private static int toWeekDay(int calWeekDay) {
321         if (calWeekDay == Calendar.SUNDAY) {
322             return 7;
323         } else {
324             return calWeekDay - 1;
325         }
326     }
327 
createStore(TemporalField field, Locale locale)328     private Object createStore(TemporalField field, Locale locale) {
329         Map<TextStyle, Map<Long, String>> styleMap = new HashMap<>();
330         if (field == ERA) {
331             for (TextStyle textStyle : TextStyle.values()) {
332                 if (textStyle.isStandalone()) {
333                     // Stand-alone isn't applicable to era names.
334                     continue;
335                 }
336                 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
337                         "gregory", Calendar.ERA, textStyle.toCalendarStyle(), locale);
338                 if (displayNames != null) {
339                     Map<Long, String> map = new HashMap<>();
340                     for (Entry<String, Integer> entry : displayNames.entrySet()) {
341                         map.put((long) entry.getValue(), entry.getKey());
342                     }
343                     if (!map.isEmpty()) {
344                         styleMap.put(textStyle, map);
345                     }
346                 }
347             }
348             return new LocaleStore(styleMap);
349         }
350 
351         if (field == MONTH_OF_YEAR) {
352             for (TextStyle textStyle : TextStyle.values()) {
353                 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
354                         "gregory", Calendar.MONTH, textStyle.toCalendarStyle(), locale);
355                 Map<Long, String> map = new HashMap<>();
356                 if (displayNames != null) {
357                     for (Entry<String, Integer> entry : displayNames.entrySet()) {
358                         map.put((long) (entry.getValue() + 1), entry.getKey());
359                     }
360 
361                 } else {
362                     // Narrow names may have duplicated names, such as "J" for January, Jun, July.
363                     // Get names one by one in that case.
364                     for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) {
365                         String name;
366                         name = CalendarDataUtility.retrieveJavaTimeFieldValueName(
367                                 "gregory", Calendar.MONTH, month, textStyle.toCalendarStyle(), locale);
368                         if (name == null) {
369                             break;
370                         }
371                         map.put((long) (month + 1), name);
372                     }
373                 }
374                 if (!map.isEmpty()) {
375                     styleMap.put(textStyle, map);
376                 }
377             }
378             return new LocaleStore(styleMap);
379         }
380 
381         if (field == DAY_OF_WEEK) {
382             for (TextStyle textStyle : TextStyle.values()) {
383                 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
384                         "gregory", Calendar.DAY_OF_WEEK, textStyle.toCalendarStyle(), locale);
385                 Map<Long, String> map = new HashMap<>();
386                 if (displayNames != null) {
387                     for (Entry<String, Integer> entry : displayNames.entrySet()) {
388                         map.put((long)toWeekDay(entry.getValue()), entry.getKey());
389                     }
390 
391                 } else {
392                     // Narrow names may have duplicated names, such as "S" for Sunday and Saturday.
393                     // Get names one by one in that case.
394                     for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) {
395                         String name;
396                         name = CalendarDataUtility.retrieveJavaTimeFieldValueName(
397                             "gregory", Calendar.DAY_OF_WEEK, wday, textStyle.toCalendarStyle(), locale);
398                         if (name == null) {
399                             break;
400                         }
401                         map.put((long)toWeekDay(wday), name);
402                     }
403                 }
404                 if (!map.isEmpty()) {
405                     styleMap.put(textStyle, map);
406                 }
407             }
408             return new LocaleStore(styleMap);
409         }
410 
411         if (field == AMPM_OF_DAY) {
412             for (TextStyle textStyle : TextStyle.values()) {
413                 if (textStyle.isStandalone()) {
414                     // Stand-alone isn't applicable to AM/PM.
415                     continue;
416                 }
417                 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
418                         "gregory", Calendar.AM_PM, textStyle.toCalendarStyle(), locale);
419                 if (displayNames != null) {
420                     Map<Long, String> map = new HashMap<>();
421                     for (Entry<String, Integer> entry : displayNames.entrySet()) {
422                         map.put((long) entry.getValue(), entry.getKey());
423                     }
424                     if (!map.isEmpty()) {
425                         styleMap.put(textStyle, map);
426                     }
427                 }
428             }
429             return new LocaleStore(styleMap);
430         }
431 
432         if (field == IsoFields.QUARTER_OF_YEAR) {
433             // Android-changed: Use ICU resources.
434             ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle
435                     .getBundleInstance(ICUData.ICU_BASE_NAME, locale);
436             ICUResourceBundle quartersRb = rb.getWithFallback("calendar/gregorian/quarters");
437             ICUResourceBundle formatRb = quartersRb.getWithFallback("format");
438             ICUResourceBundle standaloneRb = quartersRb.getWithFallback("stand-alone");
439             styleMap.put(TextStyle.FULL, extractQuarters(formatRb, "wide"));
440             styleMap.put(TextStyle.FULL_STANDALONE, extractQuarters(standaloneRb, "wide"));
441             styleMap.put(TextStyle.SHORT, extractQuarters(formatRb, "abbreviated"));
442             styleMap.put(TextStyle.SHORT_STANDALONE, extractQuarters(standaloneRb, "abbreviated"));
443             styleMap.put(TextStyle.NARROW, extractQuarters(formatRb, "narrow"));
444             styleMap.put(TextStyle.NARROW_STANDALONE, extractQuarters(standaloneRb, "narrow"));
445             return new LocaleStore(styleMap);
446         }
447 
448         return "";  // null marker for map
449     }
450 
extractQuarters(ICUResourceBundle rb, String key)451     private static Map<Long, String> extractQuarters(ICUResourceBundle rb, String key) {
452         String[] names = rb.getWithFallback(key).getStringArray();
453         Map<Long, String> map = new HashMap<>();
454         for (int q = 0; q < names.length; q++) {
455             map.put((long) (q + 1), names[q]);
456         }
457         return map;
458     }
459 
460     /**
461      * Helper method to create an immutable entry.
462      *
463      * @param text  the text, not null
464      * @param field  the field, not null
465      * @return the entry, not null
466      */
createEntry(A text, B field)467     private static <A, B> Entry<A, B> createEntry(A text, B field) {
468         return new SimpleImmutableEntry<>(text, field);
469     }
470 
471     // Android-changed: removed getLocalizedResource.
472 
473     /**
474      * Stores the text for a single locale.
475      * <p>
476      * Some fields have a textual representation, such as day-of-week or month-of-year.
477      * These textual representations can be captured in this class for printing
478      * and parsing.
479      * <p>
480      * This class is immutable and thread-safe.
481      */
482     static final class LocaleStore {
483         /**
484          * Map of value to text.
485          */
486         private final Map<TextStyle, Map<Long, String>> valueTextMap;
487         /**
488          * Parsable data.
489          */
490         private final Map<TextStyle, List<Entry<String, Long>>> parsable;
491 
492         /**
493          * Constructor.
494          *
495          * @param valueTextMap  the map of values to text to store, assigned and not altered, not null
496          */
LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap)497         LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) {
498             this.valueTextMap = valueTextMap;
499             Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<>();
500             List<Entry<String, Long>> allList = new ArrayList<>();
501             for (Map.Entry<TextStyle, Map<Long, String>> vtmEntry : valueTextMap.entrySet()) {
502                 Map<String, Entry<String, Long>> reverse = new HashMap<>();
503                 for (Map.Entry<Long, String> entry : vtmEntry.getValue().entrySet()) {
504                     if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) {
505                         // TODO: BUG: this has no effect
506                         continue;  // not parsable, try next style
507                     }
508                 }
509                 List<Entry<String, Long>> list = new ArrayList<>(reverse.values());
510                 Collections.sort(list, COMPARATOR);
511                 map.put(vtmEntry.getKey(), list);
512                 allList.addAll(list);
513                 map.put(null, allList);
514             }
515             Collections.sort(allList, COMPARATOR);
516             this.parsable = map;
517         }
518 
519         /**
520          * Gets the text for the specified field value, locale and style
521          * for the purpose of printing.
522          *
523          * @param value  the value to get text for, not null
524          * @param style  the style to get text for, not null
525          * @return the text for the field value, null if no text found
526          */
getText(long value, TextStyle style)527         String getText(long value, TextStyle style) {
528             Map<Long, String> map = valueTextMap.get(style);
529             return map != null ? map.get(value) : null;
530         }
531 
532         /**
533          * Gets an iterator of text to field for the specified style for the purpose of parsing.
534          * <p>
535          * The iterator must be returned in order from the longest text to the shortest.
536          *
537          * @param style  the style to get text for, null for all parsable text
538          * @return the iterator of text to field pairs, in order from longest text to shortest text,
539          *  null if the style is not parsable
540          */
getTextIterator(TextStyle style)541         Iterator<Entry<String, Long>> getTextIterator(TextStyle style) {
542             List<Entry<String, Long>> list = parsable.get(style);
543             return list != null ? list.iterator() : null;
544         }
545     }
546 }
547