001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017package org.apache.logging.log4j.core.util.datetime; 018 019import java.io.IOException; 020import java.io.ObjectInputStream; 021import java.io.Serializable; 022import java.text.DateFormat; 023import java.text.DateFormatSymbols; 024import java.text.FieldPosition; 025import java.util.ArrayList; 026import java.util.Calendar; 027import java.util.Date; 028import java.util.List; 029import java.util.Locale; 030import java.util.TimeZone; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.concurrent.ConcurrentMap; 033 034import org.apache.logging.log4j.core.util.Throwables; 035 036/** 037 * <p>FastDatePrinter is a fast and thread-safe version of 038 * {@link java.text.SimpleDateFormat}.</p> 039 * 040 * <p>To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} 041 * or another variation of the factory methods of {@link FastDateFormat}.</p> 042 * 043 * <p>Since FastDatePrinter is thread safe, you can use a static member instance:</p> 044 * <code> 045 * private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd"); 046 * </code> 047 * 048 * <p>This class can be used as a direct replacement to 049 * {@code SimpleDateFormat} in most formatting situations. 050 * This class is especially useful in multi-threaded server environments. 051 * {@code SimpleDateFormat} is not thread-safe in any JDK version, 052 * nor will it be as Sun have closed the bug/RFE. 053 * </p> 054 * 055 * <p>Only formatting is supported by this class, but all patterns are compatible with 056 * SimpleDateFormat (except time zones and some year patterns - see below).</p> 057 * 058 * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent 059 * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}). 060 * This pattern letter can be used here (on all JDK versions).</p> 061 * 062 * <p>In addition, the pattern {@code 'ZZ'} has been made to represent 063 * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}). 064 * This introduces a minor incompatibility with Java 1.4, but at a gain of 065 * useful functionality.</p> 066 * 067 * <p>Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}. 068 * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using 069 * one of the {@code 'X'} formats is recommended. 070 * 071 * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of 072 * pattern letters is 2, the year is truncated to 2 digits; otherwise it is 073 * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or 074 * 'YYY' will be formatted as '2003', while it was '03' in former Java 075 * versions. FastDatePrinter implements the behavior of Java 7.</p> 076 * 077 * <p> 078 * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>. 079 * </p> 080 * 081 * @since Apache Commons Lang 3.2 082 * @see FastDateParser 083 */ 084public class FastDatePrinter implements DatePrinter, Serializable { 085 // A lot of the speed in this class comes from caching, but some comes 086 // from the special int to StringBuffer conversion. 087 // 088 // The following produces a padded 2 digit number: 089 // buffer.append((char)(value / 10 + '0')); 090 // buffer.append((char)(value % 10 + '0')); 091 // 092 // Note that the fastest append to StringBuffer is a single char (used here). 093 // Note that Integer.toString() is not called, the conversion is simply 094 // taking the value and adding (mathematically) the ASCII value for '0'. 095 // So, don't change this code! It works and is very fast. 096 097 /** 098 * Required for serialization support. 099 * 100 * @see java.io.Serializable 101 */ 102 private static final long serialVersionUID = 1L; 103 104 /** 105 * FULL locale dependent date or time style. 106 */ 107 public static final int FULL = DateFormat.FULL; 108 /** 109 * LONG locale dependent date or time style. 110 */ 111 public static final int LONG = DateFormat.LONG; 112 /** 113 * MEDIUM locale dependent date or time style. 114 */ 115 public static final int MEDIUM = DateFormat.MEDIUM; 116 /** 117 * SHORT locale dependent date or time style. 118 */ 119 public static final int SHORT = DateFormat.SHORT; 120 121 /** 122 * The pattern. 123 */ 124 private final String mPattern; 125 /** 126 * The time zone. 127 */ 128 private final TimeZone mTimeZone; 129 /** 130 * The locale. 131 */ 132 private final Locale mLocale; 133 /** 134 * The parsed rules. 135 */ 136 private transient Rule[] mRules; 137 /** 138 * The estimated maximum length. 139 */ 140 private transient int mMaxLengthEstimate; 141 142 // Constructor 143 //----------------------------------------------------------------------- 144 /** 145 * <p>Constructs a new FastDatePrinter.</p> 146 * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the 147 * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance. 148 * 149 * @param pattern {@link java.text.SimpleDateFormat} compatible pattern 150 * @param timeZone non-null time zone to use 151 * @param locale non-null locale to use 152 * @throws NullPointerException if pattern, timeZone, or locale is null. 153 */ 154 protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) { 155 mPattern = pattern; 156 mTimeZone = timeZone; 157 mLocale = locale; 158 159 init(); 160 } 161 162 /** 163 * <p>Initializes the instance for first use.</p> 164 */ 165 private void init() { 166 final List<Rule> rulesList = parsePattern(); 167 mRules = rulesList.toArray(new Rule[rulesList.size()]); 168 169 int len = 0; 170 for (int i=mRules.length; --i >= 0; ) { 171 len += mRules[i].estimateLength(); 172 } 173 174 mMaxLengthEstimate = len; 175 } 176 177 // Parse the pattern 178 //----------------------------------------------------------------------- 179 /** 180 * <p>Returns a list of Rules given a pattern.</p> 181 * 182 * @return a {@code List} of Rule objects 183 * @throws IllegalArgumentException if pattern is invalid 184 */ 185 protected List<Rule> parsePattern() { 186 final DateFormatSymbols symbols = new DateFormatSymbols(mLocale); 187 final List<Rule> rules = new ArrayList<>(); 188 189 final String[] ERAs = symbols.getEras(); 190 final String[] months = symbols.getMonths(); 191 final String[] shortMonths = symbols.getShortMonths(); 192 final String[] weekdays = symbols.getWeekdays(); 193 final String[] shortWeekdays = symbols.getShortWeekdays(); 194 final String[] AmPmStrings = symbols.getAmPmStrings(); 195 196 final int length = mPattern.length(); 197 final int[] indexRef = new int[1]; 198 199 for (int i = 0; i < length; i++) { 200 indexRef[0] = i; 201 final String token = parseToken(mPattern, indexRef); 202 i = indexRef[0]; 203 204 final int tokenLen = token.length(); 205 if (tokenLen == 0) { 206 break; 207 } 208 209 Rule rule; 210 final char c = token.charAt(0); 211 212 switch (c) { 213 case 'G': // era designator (text) 214 rule = new TextField(Calendar.ERA, ERAs); 215 break; 216 case 'y': // year (number) 217 case 'Y': // week year 218 if (tokenLen == 2) { 219 rule = TwoDigitYearField.INSTANCE; 220 } else { 221 rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen); 222 } 223 if (c == 'Y') { 224 rule = new WeekYear((NumberRule) rule); 225 } 226 break; 227 case 'M': // month in year (text and number) 228 if (tokenLen >= 4) { 229 rule = new TextField(Calendar.MONTH, months); 230 } else if (tokenLen == 3) { 231 rule = new TextField(Calendar.MONTH, shortMonths); 232 } else if (tokenLen == 2) { 233 rule = TwoDigitMonthField.INSTANCE; 234 } else { 235 rule = UnpaddedMonthField.INSTANCE; 236 } 237 break; 238 case 'd': // day in month (number) 239 rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); 240 break; 241 case 'h': // hour in am/pm (number, 1..12) 242 rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen)); 243 break; 244 case 'H': // hour in day (number, 0..23) 245 rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); 246 break; 247 case 'm': // minute in hour (number) 248 rule = selectNumberRule(Calendar.MINUTE, tokenLen); 249 break; 250 case 's': // second in minute (number) 251 rule = selectNumberRule(Calendar.SECOND, tokenLen); 252 break; 253 case 'S': // millisecond (number) 254 rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); 255 break; 256 case 'E': // day in week (text) 257 rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays); 258 break; 259 case 'u': // day in week (number) 260 rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen)); 261 break; 262 case 'D': // day in year (number) 263 rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); 264 break; 265 case 'F': // day of week in month (number) 266 rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); 267 break; 268 case 'w': // week in year (number) 269 rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); 270 break; 271 case 'W': // week in month (number) 272 rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); 273 break; 274 case 'a': // am/pm marker (text) 275 rule = new TextField(Calendar.AM_PM, AmPmStrings); 276 break; 277 case 'k': // hour in day (1..24) 278 rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen)); 279 break; 280 case 'K': // hour in am/pm (0..11) 281 rule = selectNumberRule(Calendar.HOUR, tokenLen); 282 break; 283 case 'X': // ISO 8601 284 rule = Iso8601_Rule.getRule(tokenLen); 285 break; 286 case 'z': // time zone (text) 287 if (tokenLen >= 4) { 288 rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.LONG); 289 } else { 290 rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.SHORT); 291 } 292 break; 293 case 'Z': // time zone (value) 294 if (tokenLen == 1) { 295 rule = TimeZoneNumberRule.INSTANCE_NO_COLON; 296 } else if (tokenLen == 2) { 297 rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; 298 } else { 299 rule = TimeZoneNumberRule.INSTANCE_COLON; 300 } 301 break; 302 case '\'': // literal text 303 final String sub = token.substring(1); 304 if (sub.length() == 1) { 305 rule = new CharacterLiteral(sub.charAt(0)); 306 } else { 307 rule = new StringLiteral(sub); 308 } 309 break; 310 default: 311 throw new IllegalArgumentException("Illegal pattern component: " + token); 312 } 313 314 rules.add(rule); 315 } 316 317 return rules; 318 } 319 320 /** 321 * <p>Performs the parsing of tokens.</p> 322 * 323 * @param pattern the pattern 324 * @param indexRef index references 325 * @return parsed token 326 */ 327 protected String parseToken(final String pattern, final int[] indexRef) { 328 final StringBuilder buf = new StringBuilder(); 329 330 int i = indexRef[0]; 331 final int length = pattern.length(); 332 333 char c = pattern.charAt(i); 334 if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { 335 // Scan a run of the same character, which indicates a time 336 // pattern. 337 buf.append(c); 338 339 while (i + 1 < length) { 340 final char peek = pattern.charAt(i + 1); 341 if (peek == c) { 342 buf.append(c); 343 i++; 344 } else { 345 break; 346 } 347 } 348 } else { 349 // This will identify token as text. 350 buf.append('\''); 351 352 boolean inLiteral = false; 353 354 for (; i < length; i++) { 355 c = pattern.charAt(i); 356 357 if (c == '\'') { 358 if (i + 1 < length && pattern.charAt(i + 1) == '\'') { 359 // '' is treated as escaped ' 360 i++; 361 buf.append(c); 362 } else { 363 inLiteral = !inLiteral; 364 } 365 } else if (!inLiteral && 366 (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { 367 i--; 368 break; 369 } else { 370 buf.append(c); 371 } 372 } 373 } 374 375 indexRef[0] = i; 376 return buf.toString(); 377 } 378 379 /** 380 * <p>Gets an appropriate rule for the padding required.</p> 381 * 382 * @param field the field to get a rule for 383 * @param padding the padding required 384 * @return a new rule with the correct padding 385 */ 386 protected NumberRule selectNumberRule(final int field, final int padding) { 387 switch (padding) { 388 case 1: 389 return new UnpaddedNumberField(field); 390 case 2: 391 return new TwoDigitNumberField(field); 392 default: 393 return new PaddedNumberField(field, padding); 394 } 395 } 396 397 // Format methods 398 //----------------------------------------------------------------------- 399 /** 400 * <p>Formats a {@code Date}, {@code Calendar} or 401 * {@code Long} (milliseconds) object.</p> 402 * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}, or {{@link #format(Object)} 403 * @param obj the object to format 404 * @param toAppendTo the buffer to append to 405 * @param pos the position - ignored 406 * @return the buffer passed in 407 */ 408 @Deprecated 409 @Override 410 public StringBuilder format(final Object obj, final StringBuilder toAppendTo, final FieldPosition pos) { 411 if (obj instanceof Date) { 412 return format((Date) obj, toAppendTo); 413 } else if (obj instanceof Calendar) { 414 return format((Calendar) obj, toAppendTo); 415 } else if (obj instanceof Long) { 416 return format(((Long) obj).longValue(), toAppendTo); 417 } else { 418 throw new IllegalArgumentException("Unknown class: " + 419 (obj == null ? "<null>" : obj.getClass().getName())); 420 } 421 } 422 423 /** 424 * <p>Formats a {@code Date}, {@code Calendar} or 425 * {@code Long} (milliseconds) object.</p> 426 * @since 3.5 427 * @param obj the object to format 428 * @return The formatted value. 429 */ 430 String format(final Object obj) { 431 if (obj instanceof Date) { 432 return format((Date) obj); 433 } else if (obj instanceof Calendar) { 434 return format((Calendar) obj); 435 } else if (obj instanceof Long) { 436 return format(((Long) obj).longValue()); 437 } else { 438 throw new IllegalArgumentException("Unknown class: " + 439 (obj == null ? "<null>" : obj.getClass().getName())); 440 } 441 } 442 443 /* (non-Javadoc) 444 * @see org.apache.commons.lang3.time.DatePrinter#format(long) 445 */ 446 @Override 447 public String format(final long millis) { 448 final Calendar c = newCalendar(); 449 c.setTimeInMillis(millis); 450 return applyRulesToString(c); 451 } 452 453 /** 454 * Creates a String representation of the given Calendar by applying the rules of this printer to it. 455 * @param c the Calender to apply the rules to. 456 * @return a String representation of the given Calendar. 457 */ 458 private String applyRulesToString(final Calendar c) { 459 return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString(); 460 } 461 462 /** 463 * Creation method for new calender instances. 464 * @return a new Calendar instance. 465 */ 466 private Calendar newCalendar() { 467 return Calendar.getInstance(mTimeZone, mLocale); 468 } 469 470 /* (non-Javadoc) 471 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date) 472 */ 473 @Override 474 public String format(final Date date) { 475 final Calendar c = newCalendar(); 476 c.setTime(date); 477 return applyRulesToString(c); 478 } 479 480 /* (non-Javadoc) 481 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar) 482 */ 483 @Override 484 public String format(final Calendar calendar) { 485 return format(calendar, new StringBuilder(mMaxLengthEstimate)).toString(); 486 } 487 488 /* (non-Javadoc) 489 * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.Appendable) 490 */ 491 @Override 492 public <B extends Appendable> B format(final long millis, final B buf) { 493 final Calendar c = newCalendar(); 494 c.setTimeInMillis(millis); 495 return applyRules(c, buf); 496 } 497 498 /* (non-Javadoc) 499 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.Appendable) 500 */ 501 @Override 502 public <B extends Appendable> B format(final Date date, final B buf) { 503 final Calendar c = newCalendar(); 504 c.setTime(date); 505 return applyRules(c, buf); 506 } 507 508 /* (non-Javadoc) 509 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.Appendable) 510 */ 511 @Override 512 public <B extends Appendable> B format(Calendar calendar, final B buf) { 513 // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored 514 if(!calendar.getTimeZone().equals(mTimeZone)) { 515 calendar = (Calendar)calendar.clone(); 516 calendar.setTimeZone(mTimeZone); 517 } 518 return applyRules(calendar, buf); 519 } 520 521 /** 522 * Performs the formatting by applying the rules to the 523 * specified calendar. 524 * 525 * @param calendar the calendar to format 526 * @param buf the buffer to format into 527 * @return the specified string buffer 528 * 529 * @deprecated use {@link #format(Calendar)} or {@link #format(Calendar, Appendable)} 530 */ 531 @Deprecated 532 protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) { 533 return (StringBuffer) applyRules(calendar, (Appendable)buf); 534 } 535 536 /** 537 * <p>Performs the formatting by applying the rules to the 538 * specified calendar.</p> 539 * 540 * @param calendar the calendar to format 541 * @param buf the buffer to format into 542 * @param <B> the Appendable class type, usually StringBuilder or StringBuffer. 543 * @return the specified string buffer 544 */ 545 private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) { 546 try { 547 for (final Rule rule : mRules) { 548 rule.appendTo(buf, calendar); 549 } 550 } catch (final IOException ioe) { 551 Throwables.rethrow(ioe); 552 } 553 return buf; 554 } 555 556 // Accessors 557 //----------------------------------------------------------------------- 558 /* (non-Javadoc) 559 * @see org.apache.commons.lang3.time.DatePrinter#getPattern() 560 */ 561 @Override 562 public String getPattern() { 563 return mPattern; 564 } 565 566 /* (non-Javadoc) 567 * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone() 568 */ 569 @Override 570 public TimeZone getTimeZone() { 571 return mTimeZone; 572 } 573 574 /* (non-Javadoc) 575 * @see org.apache.commons.lang3.time.DatePrinter#getLocale() 576 */ 577 @Override 578 public Locale getLocale() { 579 return mLocale; 580 } 581 582 /** 583 * <p>Gets an estimate for the maximum string length that the 584 * formatter will produce.</p> 585 * 586 * <p>The actual formatted length will almost always be less than or 587 * equal to this amount.</p> 588 * 589 * @return the maximum formatted length 590 */ 591 public int getMaxLengthEstimate() { 592 return mMaxLengthEstimate; 593 } 594 595 // Basics 596 //----------------------------------------------------------------------- 597 /** 598 * <p>Compares two objects for equality.</p> 599 * 600 * @param obj the object to compare to 601 * @return {@code true} if equal 602 */ 603 @Override 604 public boolean equals(final Object obj) { 605 if (obj instanceof FastDatePrinter == false) { 606 return false; 607 } 608 final FastDatePrinter other = (FastDatePrinter) obj; 609 return mPattern.equals(other.mPattern) 610 && mTimeZone.equals(other.mTimeZone) 611 && mLocale.equals(other.mLocale); 612 } 613 614 /** 615 * <p>Returns a hash code compatible with equals.</p> 616 * 617 * @return a hash code compatible with equals 618 */ 619 @Override 620 public int hashCode() { 621 return mPattern.hashCode() + 13 * (mTimeZone.hashCode() + 13 * mLocale.hashCode()); 622 } 623 624 /** 625 * <p>Gets a debugging string version of this formatter.</p> 626 * 627 * @return a debugging string 628 */ 629 @Override 630 public String toString() { 631 return "FastDatePrinter[" + mPattern + "," + mLocale + "," + mTimeZone.getID() + "]"; 632 } 633 634 // Serializing 635 //----------------------------------------------------------------------- 636 /** 637 * Create the object after serialization. This implementation reinitializes the 638 * transient properties. 639 * 640 * @param in ObjectInputStream from which the object is being deserialized. 641 * @throws IOException if there is an IO issue. 642 * @throws ClassNotFoundException if a class cannot be found. 643 */ 644 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 645 in.defaultReadObject(); 646 init(); 647 } 648 649 /** 650 * Appends two digits to the given buffer. 651 * 652 * @param buffer the buffer to append to. 653 * @param value the value to append digits from. 654 */ 655 private static void appendDigits(final Appendable buffer, final int value) throws IOException { 656 buffer.append((char)(value / 10 + '0')); 657 buffer.append((char)(value % 10 + '0')); 658 } 659 660 private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3 661 662 /** 663 * Appends all digits to the given buffer. 664 * 665 * @param buffer the buffer to append to. 666 * @param value the value to append digits from. 667 */ 668 private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException { 669 // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array 670 // see LANG-1248 671 if (value < 10000) { 672 // less memory allocation path works for four digits or less 673 674 int nDigits = 4; 675 if (value < 1000) { 676 --nDigits; 677 if (value < 100) { 678 --nDigits; 679 if (value < 10) { 680 --nDigits; 681 } 682 } 683 } 684 // left zero pad 685 for (int i = minFieldWidth - nDigits; i > 0; --i) { 686 buffer.append('0'); 687 } 688 689 switch (nDigits) { 690 case 4: 691 buffer.append((char) (value / 1000 + '0')); 692 value %= 1000; 693 case 3: 694 if (value >= 100) { 695 buffer.append((char) (value / 100 + '0')); 696 value %= 100; 697 } else { 698 buffer.append('0'); 699 } 700 case 2: 701 if (value >= 10) { 702 buffer.append((char) (value / 10 + '0')); 703 value %= 10; 704 } else { 705 buffer.append('0'); 706 } 707 case 1: 708 buffer.append((char) (value + '0')); 709 } 710 } else { 711 // more memory allocation path works for any digits 712 713 // build up decimal representation in reverse 714 final char[] work = new char[MAX_DIGITS]; 715 int digit = 0; 716 while (value != 0) { 717 work[digit++] = (char) (value % 10 + '0'); 718 value = value / 10; 719 } 720 721 // pad with zeros 722 while (digit < minFieldWidth) { 723 buffer.append('0'); 724 --minFieldWidth; 725 } 726 727 // reverse 728 while (--digit >= 0) { 729 buffer.append(work[digit]); 730 } 731 } 732 } 733 734 // Rules 735 //----------------------------------------------------------------------- 736 /** 737 * <p>Inner class defining a rule.</p> 738 */ 739 private interface Rule { 740 /** 741 * Returns the estimated length of the result. 742 * 743 * @return the estimated length 744 */ 745 int estimateLength(); 746 747 /** 748 * Appends the value of the specified calendar to the output buffer based on the rule implementation. 749 * 750 * @param buf the output buffer 751 * @param calendar calendar to be appended 752 * @throws IOException if an I/O error occurs 753 */ 754 void appendTo(Appendable buf, Calendar calendar) throws IOException; 755 } 756 757 /** 758 * <p>Inner class defining a numeric rule.</p> 759 */ 760 private interface NumberRule extends Rule { 761 /** 762 * Appends the specified value to the output buffer based on the rule implementation. 763 * 764 * @param buffer the output buffer 765 * @param value the value to be appended 766 * @throws IOException if an I/O error occurs 767 */ 768 void appendTo(Appendable buffer, int value) throws IOException; 769 } 770 771 /** 772 * <p>Inner class to output a constant single character.</p> 773 */ 774 private static class CharacterLiteral implements Rule { 775 private final char mValue; 776 777 /** 778 * Constructs a new instance of {@code CharacterLiteral} 779 * to hold the specified value. 780 * 781 * @param value the character literal 782 */ 783 CharacterLiteral(final char value) { 784 mValue = value; 785 } 786 787 /** 788 * {@inheritDoc} 789 */ 790 @Override 791 public int estimateLength() { 792 return 1; 793 } 794 795 /** 796 * {@inheritDoc} 797 */ 798 @Override 799 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 800 buffer.append(mValue); 801 } 802 } 803 804 /** 805 * <p>Inner class to output a constant string.</p> 806 */ 807 private static class StringLiteral implements Rule { 808 private final String mValue; 809 810 /** 811 * Constructs a new instance of {@code StringLiteral} 812 * to hold the specified value. 813 * 814 * @param value the string literal 815 */ 816 StringLiteral(final String value) { 817 mValue = value; 818 } 819 820 /** 821 * {@inheritDoc} 822 */ 823 @Override 824 public int estimateLength() { 825 return mValue.length(); 826 } 827 828 /** 829 * {@inheritDoc} 830 */ 831 @Override 832 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 833 buffer.append(mValue); 834 } 835 } 836 837 /** 838 * <p>Inner class to output one of a set of values.</p> 839 */ 840 private static class TextField implements Rule { 841 private final int mField; 842 private final String[] mValues; 843 844 /** 845 * Constructs an instance of {@code TextField} 846 * with the specified field and values. 847 * 848 * @param field the field 849 * @param values the field values 850 */ 851 TextField(final int field, final String[] values) { 852 mField = field; 853 mValues = values; 854 } 855 856 /** 857 * {@inheritDoc} 858 */ 859 @Override 860 public int estimateLength() { 861 int max = 0; 862 for (int i=mValues.length; --i >= 0; ) { 863 final int len = mValues[i].length(); 864 if (len > max) { 865 max = len; 866 } 867 } 868 return max; 869 } 870 871 /** 872 * {@inheritDoc} 873 */ 874 @Override 875 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 876 buffer.append(mValues[calendar.get(mField)]); 877 } 878 } 879 880 /** 881 * <p>Inner class to output an unpadded number.</p> 882 */ 883 private static class UnpaddedNumberField implements NumberRule { 884 private final int mField; 885 886 /** 887 * Constructs an instance of {@code UnpadedNumberField} with the specified field. 888 * 889 * @param field the field 890 */ 891 UnpaddedNumberField(final int field) { 892 mField = field; 893 } 894 895 /** 896 * {@inheritDoc} 897 */ 898 @Override 899 public int estimateLength() { 900 return 4; 901 } 902 903 /** 904 * {@inheritDoc} 905 */ 906 @Override 907 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 908 appendTo(buffer, calendar.get(mField)); 909 } 910 911 /** 912 * {@inheritDoc} 913 */ 914 @Override 915 public final void appendTo(final Appendable buffer, final int value) throws IOException { 916 if (value < 10) { 917 buffer.append((char)(value + '0')); 918 } else if (value < 100) { 919 appendDigits(buffer, value); 920 } else { 921 appendFullDigits(buffer, value, 1); 922 } 923 } 924 } 925 926 /** 927 * <p>Inner class to output an unpadded month.</p> 928 */ 929 private static class UnpaddedMonthField implements NumberRule { 930 static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField(); 931 932 /** 933 * Constructs an instance of {@code UnpaddedMonthField}. 934 * 935 */ 936 UnpaddedMonthField() { 937 super(); 938 } 939 940 /** 941 * {@inheritDoc} 942 */ 943 @Override 944 public int estimateLength() { 945 return 2; 946 } 947 948 /** 949 * {@inheritDoc} 950 */ 951 @Override 952 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 953 appendTo(buffer, calendar.get(Calendar.MONTH) + 1); 954 } 955 956 /** 957 * {@inheritDoc} 958 */ 959 @Override 960 public final void appendTo(final Appendable buffer, final int value) throws IOException { 961 if (value < 10) { 962 buffer.append((char)(value + '0')); 963 } else { 964 appendDigits(buffer, value); 965 } 966 } 967 } 968 969 /** 970 * <p>Inner class to output a padded number.</p> 971 */ 972 private static class PaddedNumberField implements NumberRule { 973 private final int mField; 974 private final int mSize; 975 976 /** 977 * Constructs an instance of {@code PaddedNumberField}. 978 * 979 * @param field the field 980 * @param size size of the output field 981 */ 982 PaddedNumberField(final int field, final int size) { 983 if (size < 3) { 984 // Should use UnpaddedNumberField or TwoDigitNumberField. 985 throw new IllegalArgumentException(); 986 } 987 mField = field; 988 mSize = size; 989 } 990 991 /** 992 * {@inheritDoc} 993 */ 994 @Override 995 public int estimateLength() { 996 return mSize; 997 } 998 999 /** 1000 * {@inheritDoc} 1001 */ 1002 @Override 1003 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1004 appendTo(buffer, calendar.get(mField)); 1005 } 1006 1007 /** 1008 * {@inheritDoc} 1009 */ 1010 @Override 1011 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1012 appendFullDigits(buffer, value, mSize); 1013 } 1014 } 1015 1016 /** 1017 * <p>Inner class to output a two digit number.</p> 1018 */ 1019 private static class TwoDigitNumberField implements NumberRule { 1020 private final int mField; 1021 1022 /** 1023 * Constructs an instance of {@code TwoDigitNumberField} with the specified field. 1024 * 1025 * @param field the field 1026 */ 1027 TwoDigitNumberField(final int field) { 1028 mField = field; 1029 } 1030 1031 /** 1032 * {@inheritDoc} 1033 */ 1034 @Override 1035 public int estimateLength() { 1036 return 2; 1037 } 1038 1039 /** 1040 * {@inheritDoc} 1041 */ 1042 @Override 1043 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1044 appendTo(buffer, calendar.get(mField)); 1045 } 1046 1047 /** 1048 * {@inheritDoc} 1049 */ 1050 @Override 1051 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1052 if (value < 100) { 1053 appendDigits(buffer, value); 1054 } else { 1055 appendFullDigits(buffer, value, 2); 1056 } 1057 } 1058 } 1059 1060 /** 1061 * <p>Inner class to output a two digit year.</p> 1062 */ 1063 private static class TwoDigitYearField implements NumberRule { 1064 static final TwoDigitYearField INSTANCE = new TwoDigitYearField(); 1065 1066 /** 1067 * Constructs an instance of {@code TwoDigitYearField}. 1068 */ 1069 TwoDigitYearField() { 1070 super(); 1071 } 1072 1073 /** 1074 * {@inheritDoc} 1075 */ 1076 @Override 1077 public int estimateLength() { 1078 return 2; 1079 } 1080 1081 /** 1082 * {@inheritDoc} 1083 */ 1084 @Override 1085 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1086 appendTo(buffer, calendar.get(Calendar.YEAR) % 100); 1087 } 1088 1089 /** 1090 * {@inheritDoc} 1091 */ 1092 @Override 1093 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1094 appendDigits(buffer, value); 1095 } 1096 } 1097 1098 /** 1099 * <p>Inner class to output a two digit month.</p> 1100 */ 1101 private static class TwoDigitMonthField implements NumberRule { 1102 static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField(); 1103 1104 /** 1105 * Constructs an instance of {@code TwoDigitMonthField}. 1106 */ 1107 TwoDigitMonthField() { 1108 super(); 1109 } 1110 1111 /** 1112 * {@inheritDoc} 1113 */ 1114 @Override 1115 public int estimateLength() { 1116 return 2; 1117 } 1118 1119 /** 1120 * {@inheritDoc} 1121 */ 1122 @Override 1123 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1124 appendTo(buffer, calendar.get(Calendar.MONTH) + 1); 1125 } 1126 1127 /** 1128 * {@inheritDoc} 1129 */ 1130 @Override 1131 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1132 appendDigits(buffer, value); 1133 } 1134 } 1135 1136 /** 1137 * <p>Inner class to output the twelve hour field.</p> 1138 */ 1139 private static class TwelveHourField implements NumberRule { 1140 private final NumberRule mRule; 1141 1142 /** 1143 * Constructs an instance of {@code TwelveHourField} with the specified 1144 * {@code NumberRule}. 1145 * 1146 * @param rule the rule 1147 */ 1148 TwelveHourField(final NumberRule rule) { 1149 mRule = rule; 1150 } 1151 1152 /** 1153 * {@inheritDoc} 1154 */ 1155 @Override 1156 public int estimateLength() { 1157 return mRule.estimateLength(); 1158 } 1159 1160 /** 1161 * {@inheritDoc} 1162 */ 1163 @Override 1164 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1165 int value = calendar.get(Calendar.HOUR); 1166 if (value == 0) { 1167 value = calendar.getLeastMaximum(Calendar.HOUR) + 1; 1168 } 1169 mRule.appendTo(buffer, value); 1170 } 1171 1172 /** 1173 * {@inheritDoc} 1174 */ 1175 @Override 1176 public void appendTo(final Appendable buffer, final int value) throws IOException { 1177 mRule.appendTo(buffer, value); 1178 } 1179 } 1180 1181 /** 1182 * <p>Inner class to output the twenty four hour field.</p> 1183 */ 1184 private static class TwentyFourHourField implements NumberRule { 1185 private final NumberRule mRule; 1186 1187 /** 1188 * Constructs an instance of {@code TwentyFourHourField} with the specified 1189 * {@code NumberRule}. 1190 * 1191 * @param rule the rule 1192 */ 1193 TwentyFourHourField(final NumberRule rule) { 1194 mRule = rule; 1195 } 1196 1197 /** 1198 * {@inheritDoc} 1199 */ 1200 @Override 1201 public int estimateLength() { 1202 return mRule.estimateLength(); 1203 } 1204 1205 /** 1206 * {@inheritDoc} 1207 */ 1208 @Override 1209 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1210 int value = calendar.get(Calendar.HOUR_OF_DAY); 1211 if (value == 0) { 1212 value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; 1213 } 1214 mRule.appendTo(buffer, value); 1215 } 1216 1217 /** 1218 * {@inheritDoc} 1219 */ 1220 @Override 1221 public void appendTo(final Appendable buffer, final int value) throws IOException { 1222 mRule.appendTo(buffer, value); 1223 } 1224 } 1225 1226 /** 1227 * <p>Inner class to output the numeric day in week.</p> 1228 */ 1229 private static class DayInWeekField implements NumberRule { 1230 private final NumberRule mRule; 1231 1232 DayInWeekField(final NumberRule rule) { 1233 mRule = rule; 1234 } 1235 1236 @Override 1237 public int estimateLength() { 1238 return mRule.estimateLength(); 1239 } 1240 1241 @Override 1242 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1243 final int value = calendar.get(Calendar.DAY_OF_WEEK); 1244 mRule.appendTo(buffer, value != Calendar.SUNDAY ? value - 1 : 7); 1245 } 1246 1247 @Override 1248 public void appendTo(final Appendable buffer, final int value) throws IOException { 1249 mRule.appendTo(buffer, value); 1250 } 1251 } 1252 1253 /** 1254 * <p>Inner class to output the numeric day in week.</p> 1255 */ 1256 private static class WeekYear implements NumberRule { 1257 private final NumberRule mRule; 1258 1259 WeekYear(final NumberRule rule) { 1260 mRule = rule; 1261 } 1262 1263 @Override 1264 public int estimateLength() { 1265 return mRule.estimateLength(); 1266 } 1267 1268 @Override 1269 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1270 mRule.appendTo(buffer, calendar.getWeekYear()); 1271 } 1272 1273 @Override 1274 public void appendTo(final Appendable buffer, final int value) throws IOException { 1275 mRule.appendTo(buffer, value); 1276 } 1277 } 1278 1279 //----------------------------------------------------------------------- 1280 1281 private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache = 1282 new ConcurrentHashMap<>(7); 1283 /** 1284 * <p>Gets the time zone display name, using a cache for performance.</p> 1285 * 1286 * @param tz the zone to query 1287 * @param daylight true if daylight savings 1288 * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT} 1289 * @param locale the locale to use 1290 * @return the textual name of the time zone 1291 */ 1292 static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) { 1293 final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); 1294 String value = cTimeZoneDisplayCache.get(key); 1295 if (value == null) { 1296 // This is a very slow call, so cache the results. 1297 value = tz.getDisplayName(daylight, style, locale); 1298 final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value); 1299 if (prior != null) { 1300 value= prior; 1301 } 1302 } 1303 return value; 1304 } 1305 1306 /** 1307 * <p>Inner class to output a time zone name.</p> 1308 */ 1309 private static class TimeZoneNameRule implements Rule { 1310 private final Locale mLocale; 1311 private final int mStyle; 1312 private final String mStandard; 1313 private final String mDaylight; 1314 1315 /** 1316 * Constructs an instance of {@code TimeZoneNameRule} with the specified properties. 1317 * 1318 * @param timeZone the time zone 1319 * @param locale the locale 1320 * @param style the style 1321 */ 1322 TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) { 1323 mLocale = locale; 1324 mStyle = style; 1325 1326 mStandard = getTimeZoneDisplay(timeZone, false, style, locale); 1327 mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); 1328 } 1329 1330 /** 1331 * {@inheritDoc} 1332 */ 1333 @Override 1334 public int estimateLength() { 1335 // We have no access to the Calendar object that will be passed to 1336 // appendTo so base estimate on the TimeZone passed to the 1337 // constructor 1338 return Math.max(mStandard.length(), mDaylight.length()); 1339 } 1340 1341 /** 1342 * {@inheritDoc} 1343 */ 1344 @Override 1345 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1346 final TimeZone zone = calendar.getTimeZone(); 1347 if (calendar.get(Calendar.DST_OFFSET) != 0) { 1348 buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale)); 1349 } else { 1350 buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale)); 1351 } 1352 } 1353 } 1354 1355 /** 1356 * <p>Inner class to output a time zone as a number {@code +/-HHMM} 1357 * or {@code +/-HH:MM}.</p> 1358 */ 1359 private static class TimeZoneNumberRule implements Rule { 1360 static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true); 1361 static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false); 1362 1363 final boolean mColon; 1364 1365 /** 1366 * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties. 1367 * 1368 * @param colon add colon between HH and MM in the output if {@code true} 1369 */ 1370 TimeZoneNumberRule(final boolean colon) { 1371 mColon = colon; 1372 } 1373 1374 /** 1375 * {@inheritDoc} 1376 */ 1377 @Override 1378 public int estimateLength() { 1379 return 5; 1380 } 1381 1382 /** 1383 * {@inheritDoc} 1384 */ 1385 @Override 1386 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1387 1388 int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 1389 1390 if (offset < 0) { 1391 buffer.append('-'); 1392 offset = -offset; 1393 } else { 1394 buffer.append('+'); 1395 } 1396 1397 final int hours = offset / (60 * 60 * 1000); 1398 appendDigits(buffer, hours); 1399 1400 if (mColon) { 1401 buffer.append(':'); 1402 } 1403 1404 final int minutes = offset / (60 * 1000) - 60 * hours; 1405 appendDigits(buffer, minutes); 1406 } 1407 } 1408 1409 /** 1410 * <p>Inner class to output a time zone as a number {@code +/-HHMM} 1411 * or {@code +/-HH:MM}.</p> 1412 */ 1413 private static class Iso8601_Rule implements Rule { 1414 1415 // Sign TwoDigitHours or Z 1416 static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3); 1417 // Sign TwoDigitHours Minutes or Z 1418 static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5); 1419 // Sign TwoDigitHours : Minutes or Z 1420 static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6); 1421 1422 /** 1423 * Factory method for Iso8601_Rules. 1424 * 1425 * @param tokenLen a token indicating the length of the TimeZone String to be formatted. 1426 * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such 1427 * rule exists, an IllegalArgumentException will be thrown. 1428 */ 1429 static Iso8601_Rule getRule(final int tokenLen) { 1430 switch(tokenLen) { 1431 case 1: 1432 return Iso8601_Rule.ISO8601_HOURS; 1433 case 2: 1434 return Iso8601_Rule.ISO8601_HOURS_MINUTES; 1435 case 3: 1436 return Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; 1437 default: 1438 throw new IllegalArgumentException("invalid number of X"); 1439 } 1440 } 1441 1442 final int length; 1443 1444 /** 1445 * Constructs an instance of {@code Iso8601_Rule} with the specified properties. 1446 * 1447 * @param length The number of characters in output (unless Z is output) 1448 */ 1449 Iso8601_Rule(final int length) { 1450 this.length = length; 1451 } 1452 1453 /** 1454 * {@inheritDoc} 1455 */ 1456 @Override 1457 public int estimateLength() { 1458 return length; 1459 } 1460 1461 /** 1462 * {@inheritDoc} 1463 */ 1464 @Override 1465 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1466 int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 1467 if (offset == 0) { 1468 buffer.append("Z"); 1469 return; 1470 } 1471 1472 if (offset < 0) { 1473 buffer.append('-'); 1474 offset = -offset; 1475 } else { 1476 buffer.append('+'); 1477 } 1478 1479 final int hours = offset / (60 * 60 * 1000); 1480 appendDigits(buffer, hours); 1481 1482 if (length<5) { 1483 return; 1484 } 1485 1486 if (length==6) { 1487 buffer.append(':'); 1488 } 1489 1490 final int minutes = offset / (60 * 1000) - 60 * hours; 1491 appendDigits(buffer, minutes); 1492 } 1493 } 1494 1495 // ---------------------------------------------------------------------- 1496 /** 1497 * <p>Inner class that acts as a compound key for time zone names.</p> 1498 */ 1499 private static class TimeZoneDisplayKey { 1500 private final TimeZone mTimeZone; 1501 private final int mStyle; 1502 private final Locale mLocale; 1503 1504 /** 1505 * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties. 1506 * 1507 * @param timeZone the time zone 1508 * @param daylight adjust the style for daylight saving time if {@code true} 1509 * @param style the timezone style 1510 * @param locale the timezone locale 1511 */ 1512 TimeZoneDisplayKey(final TimeZone timeZone, 1513 final boolean daylight, final int style, final Locale locale) { 1514 mTimeZone = timeZone; 1515 if (daylight) { 1516 mStyle = style | 0x80000000; 1517 } else { 1518 mStyle = style; 1519 } 1520 mLocale = locale; 1521 } 1522 1523 /** 1524 * {@inheritDoc} 1525 */ 1526 @Override 1527 public int hashCode() { 1528 return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode(); 1529 } 1530 1531 /** 1532 * {@inheritDoc} 1533 */ 1534 @Override 1535 public boolean equals(final Object obj) { 1536 if (this == obj) { 1537 return true; 1538 } 1539 if (obj instanceof TimeZoneDisplayKey) { 1540 final TimeZoneDisplayKey other = (TimeZoneDisplayKey)obj; 1541 return 1542 mTimeZone.equals(other.mTimeZone) && 1543 mStyle == other.mStyle && 1544 mLocale.equals(other.mLocale); 1545 } 1546 return false; 1547 } 1548 } 1549}