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