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