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