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