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 // BEGIN Android-changed: Use ICU resources. 434 /* 435 // The order of keys must correspond to the TextStyle.values() order. 436 final String[] keys = { 437 "QuarterNames", 438 "standalone.QuarterNames", 439 "QuarterAbbreviations", 440 "standalone.QuarterAbbreviations", 441 "QuarterNarrows", 442 "standalone.QuarterNarrows", 443 }; 444 for (int i = 0; i < keys.length; i++) { 445 String[] names = getLocalizedResource(keys[i], locale); 446 if (names != null) { 447 Map<Long, String> map = new HashMap<>(); 448 for (int q = 0; q < names.length; q++) { 449 map.put((long) (q + 1), names[q]); 450 } 451 styleMap.put(TextStyle.values()[i], map); 452 } 453 } 454 */ 455 ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle 456 .getBundleInstance(ICUData.ICU_BASE_NAME, locale); 457 ICUResourceBundle quartersRb = rb.getWithFallback("calendar/gregorian/quarters"); 458 ICUResourceBundle formatRb = quartersRb.getWithFallback("format"); 459 ICUResourceBundle standaloneRb = quartersRb.getWithFallback("stand-alone"); 460 styleMap.put(TextStyle.FULL, extractQuarters(formatRb, "wide")); 461 styleMap.put(TextStyle.FULL_STANDALONE, extractQuarters(standaloneRb, "wide")); 462 styleMap.put(TextStyle.SHORT, extractQuarters(formatRb, "abbreviated")); 463 styleMap.put(TextStyle.SHORT_STANDALONE, extractQuarters(standaloneRb, "abbreviated")); 464 styleMap.put(TextStyle.NARROW, extractQuarters(formatRb, "narrow")); 465 styleMap.put(TextStyle.NARROW_STANDALONE, extractQuarters(standaloneRb, "narrow")); 466 // END Android-changed: Use ICU resources. 467 return new LocaleStore(styleMap); 468 } 469 470 return ""; // null marker for map 471 } 472 473 // BEGIN Android-added: Extracts a Map of quarter names from ICU resource bundle. extractQuarters(ICUResourceBundle rb, String key)474 private static Map<Long, String> extractQuarters(ICUResourceBundle rb, String key) { 475 String[] names = rb.getWithFallback(key).getStringArray(); 476 Map<Long, String> map = new HashMap<>(); 477 for (int q = 0; q < names.length; q++) { 478 map.put((long) (q + 1), names[q]); 479 } 480 return map; 481 } 482 // END Android-added: Extracts a Map of quarter names from ICU resource bundle. 483 484 /** 485 * Helper method to create an immutable entry. 486 * 487 * @param text the text, not null 488 * @param field the field, not null 489 * @return the entry, not null 490 */ createEntry(A text, B field)491 private static <A, B> Entry<A, B> createEntry(A text, B field) { 492 return new SimpleImmutableEntry<>(text, field); 493 } 494 495 // BEGIN Android-removed: Android uses ICU resources and has no LocaleProviderAdapter. 496 /** 497 * Returns the localized resource of the given key and locale, or null 498 * if no localized resource is available. 499 * 500 * @param key the key of the localized resource, not null 501 * @param locale the locale, not null 502 * @return the localized resource, or null if not available 503 * @throws NullPointerException if key or locale is null 504 */ 505 // @SuppressWarnings("unchecked") 506 // static <T> T getLocalizedResource(String key, Locale locale) { 507 // LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased() 508 // .getLocaleResources(locale); 509 // ResourceBundle rb = lr.getJavaTimeFormatData(); 510 // return rb.containsKey(key) ? (T) rb.getObject(key) : null; 511 // } 512 // END Android-removed: Android uses ICU resources and has no LocaleProviderAdapter. 513 514 /** 515 * Stores the text for a single locale. 516 * <p> 517 * Some fields have a textual representation, such as day-of-week or month-of-year. 518 * These textual representations can be captured in this class for printing 519 * and parsing. 520 * <p> 521 * This class is immutable and thread-safe. 522 */ 523 static final class LocaleStore { 524 /** 525 * Map of value to text. 526 */ 527 private final Map<TextStyle, Map<Long, String>> valueTextMap; 528 /** 529 * Parsable data. 530 */ 531 private final Map<TextStyle, List<Entry<String, Long>>> parsable; 532 533 /** 534 * Constructor. 535 * 536 * @param valueTextMap the map of values to text to store, assigned and not altered, not null 537 */ LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap)538 LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) { 539 this.valueTextMap = valueTextMap; 540 Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<>(); 541 List<Entry<String, Long>> allList = new ArrayList<>(); 542 for (Map.Entry<TextStyle, Map<Long, String>> vtmEntry : valueTextMap.entrySet()) { 543 Map<String, Entry<String, Long>> reverse = new HashMap<>(); 544 for (Map.Entry<Long, String> entry : vtmEntry.getValue().entrySet()) { 545 if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) { 546 // TODO: BUG: this has no effect 547 continue; // not parsable, try next style 548 } 549 } 550 List<Entry<String, Long>> list = new ArrayList<>(reverse.values()); 551 Collections.sort(list, COMPARATOR); 552 map.put(vtmEntry.getKey(), list); 553 allList.addAll(list); 554 map.put(null, allList); 555 } 556 Collections.sort(allList, COMPARATOR); 557 this.parsable = map; 558 } 559 560 /** 561 * Gets the text for the specified field value, locale and style 562 * for the purpose of printing. 563 * 564 * @param value the value to get text for, not null 565 * @param style the style to get text for, not null 566 * @return the text for the field value, null if no text found 567 */ getText(long value, TextStyle style)568 String getText(long value, TextStyle style) { 569 Map<Long, String> map = valueTextMap.get(style); 570 return map != null ? map.get(value) : null; 571 } 572 573 /** 574 * Gets an iterator of text to field for the specified style for the purpose of parsing. 575 * <p> 576 * The iterator must be returned in order from the longest text to the shortest. 577 * 578 * @param style the style to get text for, null for all parsable text 579 * @return the iterator of text to field pairs, in order from longest text to shortest text, 580 * null if the style is not parsable 581 */ getTextIterator(TextStyle style)582 Iterator<Entry<String, Long>> getTextIterator(TextStyle style) { 583 List<Entry<String, Long>> list = parsable.get(style); 584 return list != null ? list.iterator() : null; 585 } 586 } 587 } 588