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