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