1 /* 2 * Copyright (c) 2012, 2017, 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 static java.time.temporal.ChronoField.AMPM_OF_DAY; 65 import static java.time.temporal.ChronoField.DAY_OF_WEEK; 66 import static java.time.temporal.ChronoField.ERA; 67 import static java.time.temporal.ChronoField.MONTH_OF_YEAR; 68 69 import java.time.chrono.Chronology; 70 import java.time.chrono.IsoChronology; 71 import java.time.chrono.JapaneseChronology; 72 import java.time.temporal.ChronoField; 73 import java.time.temporal.IsoFields; 74 import java.time.temporal.TemporalField; 75 import java.util.AbstractMap.SimpleImmutableEntry; 76 import java.util.ArrayList; 77 import java.util.Calendar; 78 import java.util.Collections; 79 import java.util.Comparator; 80 import java.util.HashMap; 81 import java.util.Iterator; 82 import java.util.List; 83 import java.util.Locale; 84 import java.util.Map; 85 import java.util.Map.Entry; 86 import java.util.ResourceBundle; 87 import java.util.concurrent.ConcurrentHashMap; 88 import java.util.concurrent.ConcurrentMap; 89 90 import sun.util.locale.provider.CalendarDataUtility; 91 import sun.util.locale.provider.LocaleProviderAdapter; 92 import sun.util.locale.provider.LocaleResources; 93 94 /** 95 * A provider to obtain the textual form of a date-time field. 96 * 97 * @implSpec 98 * Implementations must be thread-safe. 99 * Implementations should cache the textual information. 100 * 101 * @since 1.8 102 */ 103 class DateTimeTextProvider { 104 105 /** Cache. */ 106 private static final ConcurrentMap<Entry<TemporalField, Locale>, Object> CACHE = new ConcurrentHashMap<>(16, 0.75f, 2); 107 /** Comparator. */ 108 private static final Comparator<Entry<String, Long>> COMPARATOR = new Comparator<Entry<String, Long>>() { 109 @Override 110 public int compare(Entry<String, Long> obj1, Entry<String, Long> obj2) { 111 return obj2.getKey().length() - obj1.getKey().length(); // longest to shortest 112 } 113 }; 114 115 // Singleton instance 116 private static final DateTimeTextProvider INSTANCE = new DateTimeTextProvider(); 117 118 DateTimeTextProvider() {} 119 120 /** 121 * Gets the provider of text. 122 * 123 * @return the provider, not null 124 */ 125 static DateTimeTextProvider getInstance() { 126 return INSTANCE; 127 } 128 129 /** 130 * Gets the text for the specified field, locale and style 131 * for the purpose of formatting. 132 * <p> 133 * The text associated with the value is returned. 134 * The null return value should be used if there is no applicable text, or 135 * if the text would be a numeric representation of the value. 136 * 137 * @param field the field to get text for, not null 138 * @param value the field value to get text for, not null 139 * @param style the style to get text for, not null 140 * @param locale the locale to get text for, not null 141 * @return the text for the field value, null if no text found 142 */ 143 public String getText(TemporalField field, long value, TextStyle style, Locale locale) { 144 Object store = findStore(field, locale); 145 if (store instanceof LocaleStore) { 146 return ((LocaleStore) store).getText(value, style); 147 } 148 return null; 149 } 150 151 /** 152 * Gets the text for the specified chrono, field, locale and style 153 * for the purpose of formatting. 154 * <p> 155 * The text associated with the value is returned. 156 * The null return value should be used if there is no applicable text, or 157 * if the text would be a numeric representation of the value. 158 * 159 * @param chrono the Chronology to get text for, not null 160 * @param field the field to get text for, not null 161 * @param value the field value to get text for, not null 162 * @param style the style to get text for, not null 163 * @param locale the locale to get text for, not null 164 * @return the text for the field value, null if no text found 165 */ 166 public String getText(Chronology chrono, TemporalField field, long value, 167 TextStyle style, Locale locale) { 168 if (chrono == IsoChronology.INSTANCE 169 || !(field instanceof ChronoField)) { 170 return getText(field, value, style, locale); 171 } 172 173 int fieldIndex; 174 int fieldValue; 175 if (field == ERA) { 176 fieldIndex = Calendar.ERA; 177 if (chrono == JapaneseChronology.INSTANCE) { 178 if (value == -999) { 179 fieldValue = 0; 180 } else { 181 fieldValue = (int) value + 2; 182 } 183 } else { 184 fieldValue = (int) value; 185 } 186 } else if (field == MONTH_OF_YEAR) { 187 fieldIndex = Calendar.MONTH; 188 fieldValue = (int) value - 1; 189 } else if (field == DAY_OF_WEEK) { 190 fieldIndex = Calendar.DAY_OF_WEEK; 191 fieldValue = (int) value + 1; 192 if (fieldValue > 7) { 193 fieldValue = Calendar.SUNDAY; 194 } 195 } else if (field == AMPM_OF_DAY) { 196 fieldIndex = Calendar.AM_PM; 197 fieldValue = (int) value; 198 } else { 199 return null; 200 } 201 return CalendarDataUtility.retrieveJavaTimeFieldValueName( 202 chrono.getCalendarType(), fieldIndex, fieldValue, style.toCalendarStyle(), locale); 203 } 204 205 /** 206 * Gets an iterator of text to field for the specified field, locale and style 207 * for the purpose of parsing. 208 * <p> 209 * The iterator must be returned in order from the longest text to the shortest. 210 * <p> 211 * The null return value should be used if there is no applicable parsable text, or 212 * if the text would be a numeric representation of the value. 213 * Text can only be parsed if all the values for that field-style-locale combination are unique. 214 * 215 * @param field the field to get text for, not null 216 * @param style the style to get text for, null for all parsable text 217 * @param locale the locale to get text for, not null 218 * @return the iterator of text to field pairs, in order from longest text to shortest text, 219 * null if the field or style is not parsable 220 */ 221 public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) { 222 Object store = findStore(field, locale); 223 if (store instanceof LocaleStore) { 224 return ((LocaleStore) store).getTextIterator(style); 225 } 226 return null; 227 } 228 229 /** 230 * Gets an iterator of text to field for the specified chrono, field, locale and style 231 * for the purpose of parsing. 232 * <p> 233 * The iterator must be returned in order from the longest text to the shortest. 234 * <p> 235 * The null return value should be used if there is no applicable parsable text, or 236 * if the text would be a numeric representation of the value. 237 * Text can only be parsed if all the values for that field-style-locale combination are unique. 238 * 239 * @param chrono the Chronology to get text for, not null 240 * @param field the field to get text for, not null 241 * @param style the style to get text for, null for all parsable text 242 * @param locale the locale to get text for, not null 243 * @return the iterator of text to field pairs, in order from longest text to shortest text, 244 * null if the field or style is not parsable 245 */ 246 public Iterator<Entry<String, Long>> getTextIterator(Chronology chrono, TemporalField field, 247 TextStyle style, Locale locale) { 248 if (chrono == IsoChronology.INSTANCE 249 || !(field instanceof ChronoField)) { 250 return getTextIterator(field, style, locale); 251 } 252 253 int fieldIndex; 254 switch ((ChronoField)field) { 255 case ERA: 256 fieldIndex = Calendar.ERA; 257 break; 258 case MONTH_OF_YEAR: 259 fieldIndex = Calendar.MONTH; 260 break; 261 case DAY_OF_WEEK: 262 fieldIndex = Calendar.DAY_OF_WEEK; 263 break; 264 case AMPM_OF_DAY: 265 fieldIndex = Calendar.AM_PM; 266 break; 267 default: 268 return null; 269 } 270 271 int calendarStyle = (style == null) ? Calendar.ALL_STYLES : style.toCalendarStyle(); 272 Map<String, Integer> map = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 273 chrono.getCalendarType(), fieldIndex, calendarStyle, locale); 274 if (map == null) { 275 return null; 276 } 277 List<Entry<String, Long>> list = new ArrayList<>(map.size()); 278 switch (fieldIndex) { 279 case Calendar.ERA: 280 for (Map.Entry<String, Integer> entry : map.entrySet()) { 281 int era = entry.getValue(); 282 if (chrono == JapaneseChronology.INSTANCE) { 283 if (era == 0) { 284 era = -999; 285 } else { 286 era -= 2; 287 } 288 } 289 list.add(createEntry(entry.getKey(), (long)era)); 290 } 291 break; 292 case Calendar.MONTH: 293 for (Map.Entry<String, Integer> entry : map.entrySet()) { 294 list.add(createEntry(entry.getKey(), (long)(entry.getValue() + 1))); 295 } 296 break; 297 case Calendar.DAY_OF_WEEK: 298 for (Map.Entry<String, Integer> entry : map.entrySet()) { 299 list.add(createEntry(entry.getKey(), (long)toWeekDay(entry.getValue()))); 300 } 301 break; 302 default: 303 for (Map.Entry<String, Integer> entry : map.entrySet()) { 304 list.add(createEntry(entry.getKey(), (long)entry.getValue())); 305 } 306 break; 307 } 308 return list.iterator(); 309 } 310 311 private Object findStore(TemporalField field, Locale locale) { 312 Entry<TemporalField, Locale> key = createEntry(field, locale); 313 Object store = CACHE.get(key); 314 if (store == null) { 315 store = createStore(field, locale); 316 CACHE.putIfAbsent(key, store); 317 store = CACHE.get(key); 318 } 319 return store; 320 } 321 322 private static int toWeekDay(int calWeekDay) { 323 if (calWeekDay == Calendar.SUNDAY) { 324 return 7; 325 } else { 326 return calWeekDay - 1; 327 } 328 } 329 330 private Object createStore(TemporalField field, Locale locale) { 331 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<>(); 332 if (field == ERA) { 333 for (TextStyle textStyle : TextStyle.values()) { 334 if (textStyle.isStandalone()) { 335 // Stand-alone isn't applicable to era names. 336 continue; 337 } 338 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 339 "gregory", Calendar.ERA, textStyle.toCalendarStyle(), locale); 340 if (displayNames != null) { 341 Map<Long, String> map = new HashMap<>(); 342 for (Entry<String, Integer> entry : displayNames.entrySet()) { 343 map.put((long) entry.getValue(), entry.getKey()); 344 } 345 if (!map.isEmpty()) { 346 styleMap.put(textStyle, map); 347 } 348 } 349 } 350 return new LocaleStore(styleMap); 351 } 352 353 if (field == MONTH_OF_YEAR) { 354 for (TextStyle textStyle : TextStyle.values()) { 355 Map<Long, String> map = new HashMap<>(); 356 // Narrow names may have duplicated names, such as "J" for January, June, July. 357 // Get names one by one in that case. 358 if ((textStyle.equals(TextStyle.NARROW) || 359 textStyle.equals(TextStyle.NARROW_STANDALONE))) { 360 for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) { 361 String name; 362 name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 363 "gregory", Calendar.MONTH, 364 month, textStyle.toCalendarStyle(), locale); 365 if (name == null) { 366 break; 367 } 368 map.put((month + 1L), name); 369 } 370 } else { 371 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 372 "gregory", Calendar.MONTH, textStyle.toCalendarStyle(), locale); 373 if (displayNames != null) { 374 for (Entry<String, Integer> entry : displayNames.entrySet()) { 375 map.put((long)(entry.getValue() + 1), entry.getKey()); 376 } 377 } else { 378 // Although probability is very less, but if other styles have duplicate names. 379 // Get names one by one in that case. 380 for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) { 381 String name; 382 name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 383 "gregory", Calendar.MONTH, month, textStyle.toCalendarStyle(), locale); 384 if (name == null) { 385 break; 386 } 387 map.put((month + 1L), name); 388 } 389 } 390 } 391 if (!map.isEmpty()) { 392 styleMap.put(textStyle, map); 393 } 394 } 395 return new LocaleStore(styleMap); 396 } 397 398 if (field == DAY_OF_WEEK) { 399 for (TextStyle textStyle : TextStyle.values()) { 400 Map<Long, String> map = new HashMap<>(); 401 // Narrow names may have duplicated names, such as "S" for Sunday and Saturday. 402 // Get names one by one in that case. 403 if ((textStyle.equals(TextStyle.NARROW) || 404 textStyle.equals(TextStyle.NARROW_STANDALONE))) { 405 for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) { 406 String name; 407 name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 408 "gregory", Calendar.DAY_OF_WEEK, 409 wday, textStyle.toCalendarStyle(), locale); 410 if (name == null) { 411 break; 412 } 413 map.put((long)toWeekDay(wday), name); 414 } 415 } else { 416 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 417 "gregory", Calendar.DAY_OF_WEEK, textStyle.toCalendarStyle(), locale); 418 if (displayNames != null) { 419 for (Entry<String, Integer> entry : displayNames.entrySet()) { 420 map.put((long)toWeekDay(entry.getValue()), entry.getKey()); 421 } 422 } else { 423 // Although probability is very less, but if other styles have duplicate names. 424 // Get names one by one in that case. 425 for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) { 426 String name; 427 name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 428 "gregory", Calendar.DAY_OF_WEEK, wday, textStyle.toCalendarStyle(), locale); 429 if (name == null) { 430 break; 431 } 432 map.put((long)toWeekDay(wday), name); 433 } 434 } 435 } 436 if (!map.isEmpty()) { 437 styleMap.put(textStyle, map); 438 } 439 } 440 return new LocaleStore(styleMap); 441 } 442 443 if (field == AMPM_OF_DAY) { 444 for (TextStyle textStyle : TextStyle.values()) { 445 if (textStyle.isStandalone()) { 446 // Stand-alone isn't applicable to AM/PM. 447 continue; 448 } 449 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 450 "gregory", Calendar.AM_PM, textStyle.toCalendarStyle(), locale); 451 if (displayNames != null) { 452 Map<Long, String> map = new HashMap<>(); 453 for (Entry<String, Integer> entry : displayNames.entrySet()) { 454 map.put((long) entry.getValue(), entry.getKey()); 455 } 456 if (!map.isEmpty()) { 457 styleMap.put(textStyle, map); 458 } 459 } 460 } 461 return new LocaleStore(styleMap); 462 } 463 464 if (field == IsoFields.QUARTER_OF_YEAR) { 465 // The order of keys must correspond to the TextStyle.values() order. 466 final String[] keys = { 467 "QuarterNames", 468 "standalone.QuarterNames", 469 "QuarterAbbreviations", 470 "standalone.QuarterAbbreviations", 471 "QuarterNarrows", 472 "standalone.QuarterNarrows", 473 }; 474 for (int i = 0; i < keys.length; i++) { 475 String[] names = getLocalizedResource(keys[i], locale); 476 if (names != null) { 477 Map<Long, String> map = new HashMap<>(); 478 for (int q = 0; q < names.length; q++) { 479 map.put((long) (q + 1), names[q]); 480 } 481 styleMap.put(TextStyle.values()[i], map); 482 } 483 } 484 return new LocaleStore(styleMap); 485 } 486 487 return ""; // null marker for map 488 } 489 490 /** 491 * Helper method to create an immutable entry. 492 * 493 * @param text the text, not null 494 * @param field the field, not null 495 * @return the entry, not null 496 */ 497 private static <A, B> Entry<A, B> createEntry(A text, B field) { 498 return new SimpleImmutableEntry<>(text, field); 499 } 500 501 /** 502 * Returns the localized resource of the given key and locale, or null 503 * if no localized resource is available. 504 * 505 * @param key the key of the localized resource, not null 506 * @param locale the locale, not null 507 * @return the localized resource, or null if not available 508 * @throws NullPointerException if key or locale is null 509 */ 510 @SuppressWarnings("unchecked") 511 static <T> T getLocalizedResource(String key, Locale locale) { 512 LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased() 513 .getLocaleResources( 514 CalendarDataUtility.findRegionOverride(locale) 515 .orElse(locale)); 516 ResourceBundle rb = lr.getJavaTimeFormatData(); 517 return rb.containsKey(key) ? (T) rb.getObject(key) : null; 518 } 519 520 /** 521 * Stores the text for a single locale. 522 * <p> 523 * Some fields have a textual representation, such as day-of-week or month-of-year. 524 * These textual representations can be captured in this class for printing 525 * and parsing. 526 * <p> 527 * This class is immutable and thread-safe. 528 */ 529 static final class LocaleStore { 530 /** 531 * Map of value to text. 532 */ 533 private final Map<TextStyle, Map<Long, String>> valueTextMap; 534 /** 535 * Parsable data. 536 */ 537 private final Map<TextStyle, List<Entry<String, Long>>> parsable; 538 539 /** 540 * Constructor. 541 * 542 * @param valueTextMap the map of values to text to store, assigned and not altered, not null 543 */ 544 LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) { 545 this.valueTextMap = valueTextMap; 546 Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<>(); 547 List<Entry<String, Long>> allList = new ArrayList<>(); 548 for (Map.Entry<TextStyle, Map<Long, String>> vtmEntry : valueTextMap.entrySet()) { 549 Map<String, Entry<String, Long>> reverse = new HashMap<>(); 550 for (Map.Entry<Long, String> entry : vtmEntry.getValue().entrySet()) { 551 if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) { 552 // TODO: BUG: this has no effect 553 continue; // not parsable, try next style 554 } 555 } 556 List<Entry<String, Long>> list = new ArrayList<>(reverse.values()); 557 Collections.sort(list, COMPARATOR); 558 map.put(vtmEntry.getKey(), list); 559 allList.addAll(list); 560 map.put(null, allList); 561 } 562 Collections.sort(allList, COMPARATOR); 563 this.parsable = map; 564 } 565 566 /** 567 * Gets the text for the specified field value, locale and style 568 * for the purpose of printing. 569 * 570 * @param value the value to get text for, not null 571 * @param style the style to get text for, not null 572 * @return the text for the field value, null if no text found 573 */ 574 String getText(long value, TextStyle style) { 575 Map<Long, String> map = valueTextMap.get(style); 576 return map != null ? map.get(value) : null; 577 } 578 579 /** 580 * Gets an iterator of text to field for the specified style for the purpose of parsing. 581 * <p> 582 * The iterator must be returned in order from the longest text to the shortest. 583 * 584 * @param style the style to get text for, null for all parsable text 585 * @return the iterator of text to field pairs, in order from longest text to shortest text, 586 * null if the style is not parsable 587 */ 588 Iterator<Entry<String, Long>> getTextIterator(TextStyle style) { 589 List<Entry<String, Long>> list = parsable.get(style); 590 return list != null ? list.iterator() : null; 591 } 592 } 593 }