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 * @version $Id: FastDatePrinter.java 1669774 2015-03-28 13:39:38Z britter $
077 * @since 3.2
078 * @see FastDateParser
079 */
080public class FastDatePrinter implements DatePrinter, Serializable {
081    // A lot of the speed in this class comes from caching, but some comes
082    // from the special int to StringBuffer conversion.
083    //
084    // The following produces a padded 2 digit number:
085    //   buffer.append((char)(value / 10 + '0'));
086    //   buffer.append((char)(value % 10 + '0'));
087    //
088    // Note that the fastest append to StringBuffer is a single char (used here).
089    // Note that Integer.toString() is not called, the conversion is simply
090    // taking the value and adding (mathematically) the ASCII value for '0'.
091    // So, don't change this code! It works and is very fast.
092
093    /**
094     * Required for serialization support.
095     *
096     * @see java.io.Serializable
097     */
098    private static final long serialVersionUID = 1L;
099
100    /**
101     * FULL locale dependent date or time style.
102     */
103    public static final int FULL = DateFormat.FULL;
104    /**
105     * LONG locale dependent date or time style.
106     */
107    public static final int LONG = DateFormat.LONG;
108    /**
109     * MEDIUM locale dependent date or time style.
110     */
111    public static final int MEDIUM = DateFormat.MEDIUM;
112    /**
113     * SHORT locale dependent date or time style.
114     */
115    public static final int SHORT = DateFormat.SHORT;
116
117    /**
118     * The pattern.
119     */
120    private final String mPattern;
121    /**
122     * The time zone.
123     */
124    private final TimeZone mTimeZone;
125    /**
126     * The locale.
127     */
128    private final Locale mLocale;
129    /**
130     * The parsed rules.
131     */
132    private transient Rule[] mRules;
133    /**
134     * The estimated maximum length.
135     */
136    private transient int mMaxLengthEstimate;
137
138    // Constructor
139    //-----------------------------------------------------------------------
140    /**
141     * <p>Constructs a new FastDatePrinter.</p>
142     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}  or another variation of the 
143     * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance.
144     *
145     * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
146     * @param timeZone  non-null time zone to use
147     * @param locale  non-null locale to use
148     * @throws NullPointerException if pattern, timeZone, or locale is null.
149     */
150    protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) {
151        mPattern = pattern;
152        mTimeZone = timeZone;
153        mLocale = locale;
154
155        init();
156    }
157
158    /**
159     * <p>Initializes the instance for first use.</p>
160     */
161    private void init() {
162        final List<Rule> rulesList = parsePattern();
163        mRules = rulesList.toArray(new Rule[rulesList.size()]);
164
165        int len = 0;
166        for (int i=mRules.length; --i >= 0; ) {
167            len += mRules[i].estimateLength();
168        }
169
170        mMaxLengthEstimate = len;
171    }
172
173    // Parse the pattern
174    //-----------------------------------------------------------------------
175    /**
176     * <p>Returns a list of Rules given a pattern.</p>
177     *
178     * @return a {@code List} of Rule objects
179     * @throws IllegalArgumentException if pattern is invalid
180     */
181    protected List<Rule> parsePattern() {
182        final DateFormatSymbols symbols = new DateFormatSymbols(mLocale);
183        final List<Rule> rules = new ArrayList<Rule>();
184
185        final String[] ERAs = symbols.getEras();
186        final String[] months = symbols.getMonths();
187        final String[] shortMonths = symbols.getShortMonths();
188        final String[] weekdays = symbols.getWeekdays();
189        final String[] shortWeekdays = symbols.getShortWeekdays();
190        final String[] AmPmStrings = symbols.getAmPmStrings();
191
192        final int length = mPattern.length();
193        final int[] indexRef = new int[1];
194
195        for (int i = 0; i < length; i++) {
196            indexRef[0] = i;
197            final String token = parseToken(mPattern, indexRef);
198            i = indexRef[0];
199
200            final int tokenLen = token.length();
201            if (tokenLen == 0) {
202                break;
203            }
204
205            Rule rule;
206            final char c = token.charAt(0);
207
208            switch (c) {
209            case 'G': // era designator (text)
210                rule = new TextField(Calendar.ERA, ERAs);
211                break;
212            case 'y': // year (number)
213                if (tokenLen == 2) {
214                    rule = TwoDigitYearField.INSTANCE;
215                } else {
216                    rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen);
217                }
218                break;
219            case 'M': // month in year (text and number)
220                if (tokenLen >= 4) {
221                    rule = new TextField(Calendar.MONTH, months);
222                } else if (tokenLen == 3) {
223                    rule = new TextField(Calendar.MONTH, shortMonths);
224                } else if (tokenLen == 2) {
225                    rule = TwoDigitMonthField.INSTANCE;
226                } else {
227                    rule = UnpaddedMonthField.INSTANCE;
228                }
229                break;
230            case 'd': // day in month (number)
231                rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);
232                break;
233            case 'h': // hour in am/pm (number, 1..12)
234                rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));
235                break;
236            case 'H': // hour in day (number, 0..23)
237                rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);
238                break;
239            case 'm': // minute in hour (number)
240                rule = selectNumberRule(Calendar.MINUTE, tokenLen);
241                break;
242            case 's': // second in minute (number)
243                rule = selectNumberRule(Calendar.SECOND, tokenLen);
244                break;
245            case 'S': // millisecond (number)
246                rule = selectNumberRule(Calendar.MILLISECOND, tokenLen);
247                break;
248            case 'E': // day in week (text)
249                rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays);
250                break;
251            case 'D': // day in year (number)
252                rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);
253                break;
254            case 'F': // day of week in month (number)
255                rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);
256                break;
257            case 'w': // week in year (number)
258                rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);
259                break;
260            case 'W': // week in month (number)
261                rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);
262                break;
263            case 'a': // am/pm marker (text)
264                rule = new TextField(Calendar.AM_PM, AmPmStrings);
265                break;
266            case 'k': // hour in day (1..24)
267                rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));
268                break;
269            case 'K': // hour in am/pm (0..11)
270                rule = selectNumberRule(Calendar.HOUR, tokenLen);
271                break;
272            case 'X': // ISO 8601 
273                rule = Iso8601_Rule.getRule(tokenLen);
274                break;    
275            case 'z': // time zone (text)
276                if (tokenLen >= 4) {
277                    rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.LONG);
278                } else {
279                    rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.SHORT);
280                }
281                break;
282            case 'Z': // time zone (value)
283                if (tokenLen == 1) {
284                    rule = TimeZoneNumberRule.INSTANCE_NO_COLON;
285                } else if (tokenLen == 2) {
286                    rule = TimeZoneNumberRule.INSTANCE_ISO_8601;
287                } else {
288                    rule = TimeZoneNumberRule.INSTANCE_COLON;
289                }
290                break;
291            case '\'': // literal text
292                final String sub = token.substring(1);
293                if (sub.length() == 1) {
294                    rule = new CharacterLiteral(sub.charAt(0));
295                } else {
296                    rule = new StringLiteral(sub);
297                }
298                break;
299            default:
300                throw new IllegalArgumentException("Illegal pattern component: " + token);
301            }
302
303            rules.add(rule);
304        }
305
306        return rules;
307    }
308
309    /**
310     * <p>Performs the parsing of tokens.</p>
311     *
312     * @param pattern  the pattern
313     * @param indexRef  index references
314     * @return parsed token
315     */
316    protected String parseToken(final String pattern, final int[] indexRef) {
317        final StringBuilder buf = new StringBuilder();
318
319        int i = indexRef[0];
320        final int length = pattern.length();
321
322        char c = pattern.charAt(i);
323        if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
324            // Scan a run of the same character, which indicates a time
325            // pattern.
326            buf.append(c);
327
328            while (i + 1 < length) {
329                final char peek = pattern.charAt(i + 1);
330                if (peek == c) {
331                    buf.append(c);
332                    i++;
333                } else {
334                    break;
335                }
336            }
337        } else {
338            // This will identify token as text.
339            buf.append('\'');
340
341            boolean inLiteral = false;
342
343            for (; i < length; i++) {
344                c = pattern.charAt(i);
345
346                if (c == '\'') {
347                    if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
348                        // '' is treated as escaped '
349                        i++;
350                        buf.append(c);
351                    } else {
352                        inLiteral = !inLiteral;
353                    }
354                } else if (!inLiteral &&
355                         (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
356                    i--;
357                    break;
358                } else {
359                    buf.append(c);
360                }
361            }
362        }
363
364        indexRef[0] = i;
365        return buf.toString();
366    }
367
368    /**
369     * <p>Gets an appropriate rule for the padding required.</p>
370     *
371     * @param field  the field to get a rule for
372     * @param padding  the padding required
373     * @return a new rule with the correct padding
374     */
375    protected NumberRule selectNumberRule(final int field, final int padding) {
376        switch (padding) {
377        case 1:
378            return new UnpaddedNumberField(field);
379        case 2:
380            return new TwoDigitNumberField(field);
381        default:
382            return new PaddedNumberField(field, padding);
383        }
384    }
385
386    // Format methods
387    //-----------------------------------------------------------------------
388    /**
389     * <p>Formats a {@code Date}, {@code Calendar} or
390     * {@code Long} (milliseconds) object.</p>
391     *
392     * @param obj  the object to format
393     * @param toAppendTo  the buffer to append to
394     * @param pos  the position - ignored
395     * @return the buffer passed in
396     */
397    @Override
398    public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
399        if (obj instanceof Date) {
400            return format((Date) obj, toAppendTo);
401        } else if (obj instanceof Calendar) {
402            return format((Calendar) obj, toAppendTo);
403        } else if (obj instanceof Long) {
404            return format(((Long) obj).longValue(), toAppendTo);
405        } else {
406            throw new IllegalArgumentException("Unknown class: " +
407                (obj == null ? "<null>" : obj.getClass().getName()));
408        }
409    }
410
411    /* (non-Javadoc)
412     * @see org.apache.commons.lang3.time.DatePrinter#format(long)
413     */
414    @Override
415    public String format(final long millis) {
416        final Calendar c = newCalendar();  // hard code GregorianCalendar
417        c.setTimeInMillis(millis);
418        return applyRulesToString(c);
419    }
420
421    /**
422     * Creates a String representation of the given Calendar by applying the rules of this printer to it.
423     * @param c the Calender to apply the rules to.
424     * @return a String representation of the given Calendar.
425     */
426    private String applyRulesToString(final Calendar c) {
427        return applyRules(c, new StringBuffer(mMaxLengthEstimate)).toString();
428    }
429
430    /**
431     * Creation method for ne calender instances.
432     * @return a new Calendar instance.
433     */
434    private GregorianCalendar newCalendar() {
435        // hard code GregorianCalendar
436        return new GregorianCalendar(mTimeZone, mLocale);
437    }
438
439    /* (non-Javadoc)
440     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date)
441     */
442    @Override
443    public String format(final Date date) {
444        final Calendar c = newCalendar();  // hard code GregorianCalendar
445        c.setTime(date);
446        return applyRulesToString(c);
447    }
448
449    /* (non-Javadoc)
450     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar)
451     */
452    @Override
453    public String format(final Calendar calendar) {
454        return format(calendar, new StringBuffer(mMaxLengthEstimate)).toString();
455    }
456
457    /* (non-Javadoc)
458     * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.StringBuffer)
459     */
460    @Override
461    public StringBuffer format(final long millis, final StringBuffer buf) {
462        return format(new Date(millis), buf);
463    }
464
465    /* (non-Javadoc)
466     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.StringBuffer)
467     */
468    @Override
469    public StringBuffer format(final Date date, final StringBuffer buf) {
470        final Calendar c = newCalendar();  // hard code GregorianCalendar
471        c.setTime(date);
472        return applyRules(c, buf);
473    }
474
475    /* (non-Javadoc)
476     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.StringBuffer)
477     */
478    @Override
479    public StringBuffer format(final Calendar calendar, final StringBuffer buf) {
480        return applyRules(calendar, 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, false);
1182        static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false, false);
1183        static final TimeZoneNumberRule INSTANCE_ISO_8601 = new TimeZoneNumberRule(true, true);
1184        
1185        final boolean mColon;
1186        final boolean mISO8601;
1187
1188        /**
1189         * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties.
1190         *
1191         * @param colon add colon between HH and MM in the output if {@code true}
1192         * @param iso8601 create an ISO 8601 format output
1193         */
1194        TimeZoneNumberRule(final boolean colon, final boolean iso8601) {
1195            mColon = colon;
1196            mISO8601 = iso8601;
1197        }
1198
1199        /**
1200         * {@inheritDoc}
1201         */
1202        @Override
1203        public int estimateLength() {
1204            return 5;
1205        }
1206
1207        /**
1208         * {@inheritDoc}
1209         */
1210        @Override
1211        public void appendTo(final StringBuffer buffer, final Calendar calendar) {
1212            if (mISO8601 && calendar.getTimeZone().getID().equals("UTC")) {
1213                buffer.append("Z");
1214                return;
1215            }
1216            
1217            int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
1218
1219            if (offset < 0) {
1220                buffer.append('-');
1221                offset = -offset;
1222            } else {
1223                buffer.append('+');
1224            }
1225
1226            final int hours = offset / (60 * 60 * 1000);
1227            appendDigits(buffer, hours);
1228
1229            if (mColon) {
1230                buffer.append(':');
1231            }
1232
1233            final int minutes = offset / (60 * 1000) - 60 * hours;
1234            appendDigits(buffer, minutes);
1235        }
1236    }
1237
1238    /**
1239     * <p>Inner class to output a time zone as a number {@code +/-HHMM}
1240     * or {@code +/-HH:MM}.</p>
1241     */
1242    private static class Iso8601_Rule implements Rule {
1243        
1244        // Sign TwoDigitHours or Z
1245        static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3);       
1246        // Sign TwoDigitHours Minutes or Z
1247        static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5);
1248        // Sign TwoDigitHours : Minutes or Z
1249        static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6);
1250
1251        /**
1252         * Factory method for Iso8601_Rules.
1253         *
1254         * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
1255         * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such
1256         *          rule exists, an IllegalArgumentException will be thrown.
1257         */
1258        static Iso8601_Rule getRule(int tokenLen) {
1259            switch(tokenLen) {
1260            case 1:
1261                return Iso8601_Rule.ISO8601_HOURS;
1262            case 2:
1263                return Iso8601_Rule.ISO8601_HOURS_MINUTES;
1264            case 3:
1265                return Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;
1266            default:
1267                throw new IllegalArgumentException("invalid number of X");                    
1268            }
1269        }        
1270        
1271        final int length;
1272
1273        /**
1274         * Constructs an instance of {@code Iso8601_Rule} with the specified properties.
1275         *
1276         * @param length The number of characters in output (unless Z is output)
1277         */
1278        Iso8601_Rule(final int length) {
1279            this.length = length;
1280        }
1281
1282        /**
1283         * {@inheritDoc}
1284         */
1285        @Override
1286        public int estimateLength() {
1287            return length;
1288        }
1289
1290        /**
1291         * {@inheritDoc}
1292         */
1293        @Override
1294        public void appendTo(final StringBuffer buffer, final Calendar calendar) {
1295            int zoneOffset = calendar.get(Calendar.ZONE_OFFSET);
1296            if (zoneOffset == 0) {
1297                buffer.append("Z");
1298                return;
1299            }
1300            
1301            int offset = zoneOffset + calendar.get(Calendar.DST_OFFSET);
1302
1303            if (offset < 0) {
1304                buffer.append('-');
1305                offset = -offset;
1306            } else {
1307                buffer.append('+');
1308            }
1309
1310            final int hours = offset / (60 * 60 * 1000);
1311            appendDigits(buffer, hours);
1312
1313            if (length<5) {
1314                return;
1315            }
1316            
1317            if (length==6) {
1318                buffer.append(':');
1319            }
1320
1321            final int minutes = offset / (60 * 1000) - 60 * hours;
1322            appendDigits(buffer, minutes);
1323        }
1324    }
1325
1326    // ----------------------------------------------------------------------
1327    /**
1328     * <p>Inner class that acts as a compound key for time zone names.</p>
1329     */
1330    private static class TimeZoneDisplayKey {
1331        private final TimeZone mTimeZone;
1332        private final int mStyle;
1333        private final Locale mLocale;
1334
1335        /**
1336         * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties.
1337         *
1338         * @param timeZone the time zone
1339         * @param daylight adjust the style for daylight saving time if {@code true}
1340         * @param style the timezone style
1341         * @param locale the timezone locale
1342         */
1343        TimeZoneDisplayKey(final TimeZone timeZone,
1344                           final boolean daylight, final int style, final Locale locale) {
1345            mTimeZone = timeZone;
1346            if (daylight) {
1347                mStyle = style | 0x80000000;
1348            } else {
1349                mStyle = style;
1350            }
1351            mLocale = locale;
1352        }
1353
1354        /**
1355         * {@inheritDoc}
1356         */
1357        @Override
1358        public int hashCode() {
1359            return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode();
1360        }
1361
1362        /**
1363         * {@inheritDoc}
1364         */
1365        @Override
1366        public boolean equals(final Object obj) {
1367            if (this == obj) {
1368                return true;
1369            }
1370            if (obj instanceof TimeZoneDisplayKey) {
1371                final TimeZoneDisplayKey other = (TimeZoneDisplayKey)obj;
1372                return
1373                    mTimeZone.equals(other.mTimeZone) &&
1374                    mStyle == other.mStyle &&
1375                    mLocale.equals(other.mLocale);
1376            }
1377            return false;
1378        }
1379    }
1380}