View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.util.datetime;
18  
19  import java.io.IOException;
20  import java.io.ObjectInputStream;
21  import java.io.Serializable;
22  import java.text.DateFormat;
23  import java.text.DateFormatSymbols;
24  import java.text.FieldPosition;
25  import java.util.ArrayList;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.TimeZone;
31  import java.util.concurrent.ConcurrentHashMap;
32  import java.util.concurrent.ConcurrentMap;
33  
34  import org.apache.logging.log4j.core.util.Throwables;
35  
36  /**
37   * <p>FastDatePrinter is a fast and thread-safe version of
38   * {@link java.text.SimpleDateFormat}.</p>
39   *
40   * <p>To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}
41   * or another variation of the factory methods of {@link FastDateFormat}.</p>
42   *
43   * <p>Since FastDatePrinter is thread safe, you can use a static member instance:</p>
44   * <code>
45   *     private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd");
46   * </code>
47   *
48   * <p>This class can be used as a direct replacement to
49   * {@code SimpleDateFormat} in most formatting situations.
50   * This class is especially useful in multi-threaded server environments.
51   * {@code SimpleDateFormat} is not thread-safe in any JDK version,
52   * nor will it be as Sun have closed the bug/RFE.
53   * </p>
54   *
55   * <p>Only formatting is supported by this class, but all patterns are compatible with
56   * SimpleDateFormat (except time zones and some year patterns - see below).</p>
57   *
58   * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
59   * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
60   * This pattern letter can be used here (on all JDK versions).</p>
61   *
62   * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
63   * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}).
64   * This introduces a minor incompatibility with Java 1.4, but at a gain of
65   * useful functionality.</p>
66   *
67   * <p>Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}.
68   * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using
69   * one of the {@code 'X'} formats is recommended.
70   *
71   * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
72   * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
73   * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
74   * 'YYY' will be formatted as '2003', while it was '03' in former Java
75   * versions. FastDatePrinter implements the behavior of Java 7.</p>
76   *
77   * <p>
78   * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
79   * </p>
80   *
81   * @since Apache Commons Lang 3.2
82   * @see FastDateParser
83   */
84  public class FastDatePrinter implements DatePrinter, Serializable {
85      // A lot of the speed in this class comes from caching, but some comes
86      // from the special int to StringBuffer conversion.
87      //
88      // The following produces a padded 2 digit number:
89      //   buffer.append((char)(value / 10 + '0'));
90      //   buffer.append((char)(value % 10 + '0'));
91      //
92      // Note that the fastest append to StringBuffer is a single char (used here).
93      // Note that Integer.toString() is not called, the conversion is simply
94      // taking the value and adding (mathematically) the ASCII value for '0'.
95      // So, don't change this code! It works and is very fast.
96  
97      /**
98       * Required for serialization support.
99       *
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 }