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