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 */
017
018package org.apache.logging.log4j.core.util.datetime;
019
020import org.apache.logging.log4j.core.time.Instant;
021
022import java.util.Arrays;
023import java.util.Calendar;
024import java.util.Objects;
025import java.util.TimeZone;
026import java.util.concurrent.TimeUnit;
027
028/**
029 * Custom time formatter that trades flexibility for performance. This formatter only supports the date patterns defined
030 * in {@link FixedFormat}. For any other date patterns use {@link FastDateFormat}.
031 * <p>
032 * Related benchmarks: /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and
033 * /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
034 */
035public class FixedDateFormat {
036
037    /**
038     * Enumeration over the supported date/time format patterns.
039     * <p>
040     * Package protected for unit tests.
041     */
042    public enum FixedFormat {
043        /**
044         * ABSOLUTE time format: {@code "HH:mm:ss,SSS"}.
045         */
046        ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1, 3),
047        /**
048         * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,nnnnnn"}.
049         */
050        ABSOLUTE_MICROS("HH:mm:ss,nnnnnn", null, 0, ':', 1, ',', 1, 6),
051        /**
052         * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,nnnnnnnnn"}.
053         */
054        ABSOLUTE_NANOS("HH:mm:ss,nnnnnnnnn", null, 0, ':', 1, ',', 1, 9),
055
056        /**
057         * ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}.
058         */
059        ABSOLUTE_PERIOD("HH:mm:ss.SSS", null, 0, ':', 1, '.', 1, 3),
060
061        /**
062         * COMPACT time format: {@code "yyyyMMddHHmmssSSS"}.
063         */
064        COMPACT("yyyyMMddHHmmssSSS", "yyyyMMdd", 0, ' ', 0, ' ', 0, 3),
065
066        /**
067         * DATE_AND_TIME time format: {@code "dd MMM yyyy HH:mm:ss,SSS"}.
068         */
069        DATE("dd MMM yyyy HH:mm:ss,SSS", "dd MMM yyyy ", 0, ':', 1, ',', 1, 3),
070
071        /**
072         * DATE_AND_TIME time format variation with period separator: {@code "dd MMM yyyy HH:mm:ss.SSS"}.
073         */
074        DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS", "dd MMM yyyy ", 0, ':', 1, '.', 1, 3),
075
076        /**
077         * DEFAULT time format: {@code "yyyy-MM-dd HH:mm:ss,SSS"}.
078         */
079        DEFAULT("yyyy-MM-dd HH:mm:ss,SSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 3),
080        /**
081         * DEFAULT time format with microsecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnn"}.
082         */
083        DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,nnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 6),
084        /**
085         * DEFAULT time format with nanosecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"}.
086         */
087        DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,nnnnnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 9),
088
089        /**
090         * DEFAULT time format variation with period separator: {@code "yyyy-MM-dd HH:mm:ss.SSS"}.
091         */
092        DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS", "yyyy-MM-dd ", 0, ':', 1, '.', 1, 3),
093
094        /**
095         * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss,SSS"}.
096         */
097        ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS", "yyyyMMdd'T'", 2, ' ', 0, ',', 1, 3),
098
099        /**
100         * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss.SSS"}.
101         */
102        ISO8601_BASIC_PERIOD("yyyyMMdd'T'HHmmss.SSS", "yyyyMMdd'T'", 2, ' ', 0, '.', 1, 3),
103
104        /**
105         * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSS"}.
106         */
107        ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3),
108
109        /**
110         * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss.SSS"}.
111         */
112        ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 3);
113
114        private static final String DEFAULT_SECOND_FRACTION_PATTERN = "SSS";
115        private static final int MILLI_FRACTION_DIGITS = DEFAULT_SECOND_FRACTION_PATTERN.length();
116        private static final char SECOND_FRACTION_PATTERN = 'n';
117
118        private final String pattern;
119        private final String datePattern;
120        private final int escapeCount;
121        private final char timeSeparatorChar;
122        private final int timeSeparatorLength;
123        private final char millisSeparatorChar;
124        private final int millisSeparatorLength;
125        private final int secondFractionDigits;
126
127        FixedFormat(final String pattern, final String datePattern, final int escapeCount, final char timeSeparator,
128                    final int timeSepLength, final char millisSeparator, final int millisSepLength,
129                    final int secondFractionDigits) {
130            this.timeSeparatorChar = timeSeparator;
131            this.timeSeparatorLength = timeSepLength;
132            this.millisSeparatorChar = millisSeparator;
133            this.millisSeparatorLength = millisSepLength;
134            this.pattern = Objects.requireNonNull(pattern);
135            this.datePattern = datePattern; // may be null
136            this.escapeCount = escapeCount;
137            this.secondFractionDigits = secondFractionDigits;
138        }
139
140        /**
141         * Returns the full pattern.
142         *
143         * @return the full pattern
144         */
145        public String getPattern() {
146            return pattern;
147        }
148
149        /**
150         * Returns the date part of the pattern.
151         *
152         * @return the date part of the pattern
153         */
154        public String getDatePattern() {
155            return datePattern;
156        }
157
158        /**
159         * Returns the FixedFormat with the name or pattern matching the specified string or {@code null} if not found.
160         *
161         * @param nameOrPattern the name or pattern to find a FixedFormat for
162         * @return the FixedFormat with the name or pattern matching the specified string
163         */
164        public static FixedFormat lookup(final String nameOrPattern) {
165            for (final FixedFormat type : FixedFormat.values()) {
166                if (type.name().equals(nameOrPattern) || type.getPattern().equals(nameOrPattern)) {
167                    return type;
168                }
169            }
170            return null;
171        }
172
173        static FixedFormat lookupIgnoringNanos(final String pattern) {
174            final int nanoStart = nanoStart(pattern);
175            if (nanoStart > 0) {
176                final String subPattern = pattern.substring(0, nanoStart) + DEFAULT_SECOND_FRACTION_PATTERN;
177                for (final FixedFormat type : FixedFormat.values()) {
178                    if (type.getPattern().equals(subPattern)) {
179                        return type;
180                    }
181                }
182            }
183            return null;
184        }
185
186        private static int nanoStart(final String pattern) {
187            final int index = pattern.indexOf(SECOND_FRACTION_PATTERN);
188            if (index >= 0) {
189                for (int i = index + 1; i < pattern.length(); i++) {
190                    if (pattern.charAt(i) != SECOND_FRACTION_PATTERN) {
191                        return -1;
192                    }
193                }
194            }
195            return index;
196        }
197
198        /**
199         * Returns the length of the resulting formatted date and time strings.
200         *
201         * @return the length of the resulting formatted date and time strings
202         */
203        public int getLength() {
204            return pattern.length() - escapeCount;
205        }
206
207        /**
208         * Returns the length of the date part of the resulting formatted string.
209         *
210         * @return the length of the date part of the resulting formatted string
211         */
212        public int getDatePatternLength() {
213            return getDatePattern() == null ? 0 : getDatePattern().length() - escapeCount;
214        }
215
216        /**
217         * Returns the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null} if the
218         * pattern does not have a date part.
219         *
220         * @return the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null}
221         */
222        public FastDateFormat getFastDateFormat() {
223            return getFastDateFormat(null);
224        }
225
226        /**
227         * Returns the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null} if the
228         * pattern does not have a date part.
229         *
230         * @param tz the time zone to use
231         * @return the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null}
232         */
233        public FastDateFormat getFastDateFormat(final TimeZone tz) {
234            return getDatePattern() == null ? null : FastDateFormat.getInstance(getDatePattern(), tz);
235        }
236
237        /**
238         * Returns the number of digits specifying the fraction of the second to show
239         * @return 3 for millisecond precision, 6 for microsecond precision or 9 for nanosecond precision
240         */
241        public int getSecondFractionDigits() {
242            return secondFractionDigits;
243        }
244    }
245
246    private final FixedFormat fixedFormat;
247    private final TimeZone timeZone;
248    private final int length;
249    private final int secondFractionDigits;
250    private final FastDateFormat fastDateFormat; // may be null
251    private final char timeSeparatorChar;
252    private final char millisSeparatorChar;
253    private final int timeSeparatorLength;
254    private final int millisSeparatorLength;
255
256    private volatile long midnightToday = 0;
257    private volatile long midnightTomorrow = 0;
258    private final int[] dstOffsets = new int[25];
259
260    // cachedDate does not need to be volatile because
261    // there is a write to a volatile field *after* cachedDate is modified,
262    // and there is a read from a volatile field *before* cachedDate is read.
263    // The Java memory model guarantees that because of the above,
264    // changes to cachedDate in one thread are visible to other threads.
265    // See http://g.oswego.edu/dl/jmm/cookbook.html
266    private char[] cachedDate; // may be null
267    private int dateLength;
268
269    /**
270     * Constructs a FixedDateFormat for the specified fixed format.
271     * <p>
272     * Package protected for unit tests.
273     *
274     * @param fixedFormat the fixed format
275     * @param tz time zone
276     */
277    FixedDateFormat(final FixedFormat fixedFormat, final TimeZone tz) {
278        this(fixedFormat, tz, fixedFormat.getSecondFractionDigits());
279    }
280
281    /**
282     * Constructs a FixedDateFormat for the specified fixed format.
283     * <p>
284     * Package protected for unit tests.
285     *
286     * @param fixedFormat the fixed format
287     * @param tz time zone
288     * @param secondFractionDigits the number of digits specifying the fraction of the second to show
289     */
290    FixedDateFormat(final FixedFormat fixedFormat, final TimeZone tz, final int secondFractionDigits) {
291        this.fixedFormat = Objects.requireNonNull(fixedFormat);
292        this.timeZone = Objects.requireNonNull(tz);
293        this.timeSeparatorChar = fixedFormat.timeSeparatorChar;
294        this.timeSeparatorLength = fixedFormat.timeSeparatorLength;
295        this.millisSeparatorChar = fixedFormat.millisSeparatorChar;
296        this.millisSeparatorLength = fixedFormat.millisSeparatorLength;
297        this.length = fixedFormat.getLength();
298        this.secondFractionDigits = Math.max(1, Math.min(9, secondFractionDigits));
299        this.fastDateFormat = fixedFormat.getFastDateFormat(tz);
300    }
301
302    public static FixedDateFormat createIfSupported(final String... options) {
303        if (options == null || options.length == 0 || options[0] == null) {
304            return new FixedDateFormat(FixedFormat.DEFAULT, TimeZone.getDefault());
305        }
306        final TimeZone tz;
307        if (options.length > 1) {
308            if (options[1] != null) {
309                tz = TimeZone.getTimeZone(options[1]);
310            } else {
311                tz = TimeZone.getDefault();
312            }
313        } else {
314            tz = TimeZone.getDefault();
315        }
316
317        final FixedFormat withNanos = FixedFormat.lookupIgnoringNanos(options[0]);
318        if (withNanos != null) {
319            final int secondFractionDigits = options[0].length() - FixedFormat.nanoStart(options[0]);
320            return new FixedDateFormat(withNanos, tz, secondFractionDigits);
321        }
322        final FixedFormat type = FixedFormat.lookup(options[0]);
323        return type == null ? null : new FixedDateFormat(type, tz);
324    }
325
326    /**
327     * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and a {@code TimeZone.getDefault()} TimeZone.
328     *
329     * @param format the format to use
330     * @return a new {@code FixedDateFormat} object
331     */
332    public static FixedDateFormat create(final FixedFormat format) {
333        return new FixedDateFormat(format, TimeZone.getDefault());
334    }
335
336    /**
337     * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and TimeZone.
338     *
339     * @param format the format to use
340     * @param tz the time zone to use
341     * @return a new {@code FixedDateFormat} object
342     */
343    public static FixedDateFormat create(final FixedFormat format, final TimeZone tz) {
344        return new FixedDateFormat(format, tz != null ? tz : TimeZone.getDefault());
345    }
346
347    /**
348     * Returns the full pattern of the selected fixed format.
349     *
350     * @return the full date-time pattern
351     */
352    public String getFormat() {
353        return fixedFormat.getPattern();
354    }
355
356    /**
357     * Returns the time zone.
358     *
359     * @return the time zone
360     */
361
362    public TimeZone getTimeZone() {
363        return timeZone;
364    }
365
366    /**
367     * <p>Returns the number of milliseconds since midnight in the time zone that this {@code FixedDateFormat}
368     * was constructed with for the specified currentTime.</p>
369     * <p>As a side effect, this method updates the cached formatted date and the cached date demarcation timestamps
370     * when the specified current time is outside the previously set demarcation timestamps for the start or end
371     * of the current day.</p>
372     * @param currentTime the current time in millis since the epoch
373     * @return the number of milliseconds since midnight for the specified time
374     */
375    // Profiling showed this method is important to log4j performance. Modify with care!
376    // 30 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
377    public long millisSinceMidnight(final long currentTime) {
378        if (currentTime >= midnightTomorrow || currentTime < midnightToday) {
379            updateMidnightMillis(currentTime);
380        }
381        return currentTime - midnightToday;
382    }
383
384    private void updateMidnightMillis(final long now) {
385        if (now >= midnightTomorrow || now < midnightToday) {
386            synchronized (this) {
387                updateCachedDate(now);
388                midnightToday = calcMidnightMillis(now, 0);
389                midnightTomorrow = calcMidnightMillis(now, 1);
390
391                updateDaylightSavingTime();
392            }
393        }
394    }
395
396    private long calcMidnightMillis(final long time, final int addDays) {
397        final Calendar cal = Calendar.getInstance(timeZone);
398        cal.setTimeInMillis(time);
399        cal.set(Calendar.HOUR_OF_DAY, 0);
400        cal.set(Calendar.MINUTE, 0);
401        cal.set(Calendar.SECOND, 0);
402        cal.set(Calendar.MILLISECOND, 0);
403        cal.add(Calendar.DATE, addDays);
404        return cal.getTimeInMillis();
405    }
406
407    private void updateDaylightSavingTime() {
408        Arrays.fill(dstOffsets, 0);
409        final int ONE_HOUR = (int) TimeUnit.HOURS.toMillis(1);
410        if (timeZone.getOffset(midnightToday) != timeZone.getOffset(midnightToday + 23 * ONE_HOUR)) {
411            for (int i = 0; i < dstOffsets.length; i++) {
412                final long time = midnightToday + i * ONE_HOUR;
413                dstOffsets[i] = timeZone.getOffset(time) - timeZone.getRawOffset();
414            }
415            if (dstOffsets[0] > dstOffsets[23]) { // clock is moved backwards.
416                // we obtain midnightTonight with Calendar.getInstance(TimeZone), so it already includes raw offset
417                for (int i = dstOffsets.length - 1; i >= 0; i--) {
418                    dstOffsets[i] -= dstOffsets[0]; //
419                }
420            }
421        }
422    }
423
424    private void updateCachedDate(final long now) {
425        if (fastDateFormat != null) {
426            final StringBuilder result = fastDateFormat.format(now, new StringBuilder());
427            cachedDate = result.toString().toCharArray();
428            dateLength = result.length();
429        }
430    }
431
432    public String formatInstant(final Instant instant) {
433        final char[] result = new char[length << 1]; // double size for locales with lengthy DateFormatSymbols
434        final int written = formatInstant(instant, result, 0);
435        return new String(result, 0, written);
436    }
437
438    public int formatInstant(final Instant instant, final char[] buffer, final int startPos) {
439        int result = format(instant.getEpochMillisecond(), buffer, startPos);
440        result -= digitsLessThanThree();
441        formatNanoOfMillisecond(instant.getNanoOfMillisecond(), buffer, startPos + result);
442        return result + digitsMorePreciseThanMillis();
443    }
444
445    private int digitsLessThanThree() { // in case user specified only 1 or 2 'n' format characters
446        return Math.max(0, FixedFormat.MILLI_FRACTION_DIGITS - secondFractionDigits);
447    }
448
449    private int digitsMorePreciseThanMillis() {
450        return Math.max(0, secondFractionDigits - FixedFormat.MILLI_FRACTION_DIGITS);
451    }
452
453    // Profiling showed this method is important to log4j performance. Modify with care!
454    // 28 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
455    public String format(final long epochMillis) {
456        final char[] result = new char[length << 1]; // double size for locales with lengthy DateFormatSymbols
457        final int written = format(epochMillis, result, 0);
458        return new String(result, 0, written);
459    }
460
461    // Profiling showed this method is important to log4j performance. Modify with care!
462    // 31 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
463    public int format(final long epochMillis, final char[] buffer, final int startPos) {
464        // Calculate values by getting the ms values first and do then
465        // calculate the hour minute and second values divisions.
466
467        // Get daytime in ms: this does fit into an int
468        // int ms = (int) (time % 86400000);
469        final int ms = (int) (millisSinceMidnight(epochMillis));
470        writeDate(buffer, startPos);
471        return writeTime(ms, buffer, startPos + dateLength) - startPos;
472    }
473
474    // Profiling showed this method is important to log4j performance. Modify with care!
475    // 22 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
476    private void writeDate(final char[] buffer, final int startPos) {
477        if (cachedDate != null) {
478            System.arraycopy(cachedDate, 0, buffer, startPos, dateLength);
479        }
480    }
481
482    // Profiling showed this method is important to log4j performance. Modify with care!
483    // 262 bytes (will be inlined when hot enough: <= -XX:FreqInlineSize=325 bytes on Linux)
484    private int writeTime(int ms, final char[] buffer, int pos) {
485        final int hourOfDay = ms / 3600000;
486        final int hours = hourOfDay + daylightSavingTime(hourOfDay) / 3600000;
487        ms -= 3600000 * hourOfDay;
488
489        final int minutes = ms / 60000;
490        ms -= 60000 * minutes;
491
492        final int seconds = ms / 1000;
493        ms -= 1000 * seconds;
494
495        // Hour
496        int temp = hours / 10;
497        buffer[pos++] = ((char) (temp + '0'));
498
499        // Do subtract to get remainder instead of doing % 10
500        buffer[pos++] = ((char) (hours - 10 * temp + '0'));
501        buffer[pos] = timeSeparatorChar;
502        pos += timeSeparatorLength;
503
504        // Minute
505        temp = minutes / 10;
506        buffer[pos++] = ((char) (temp + '0'));
507
508        // Do subtract to get remainder instead of doing % 10
509        buffer[pos++] = ((char) (minutes - 10 * temp + '0'));
510        buffer[pos] = timeSeparatorChar;
511        pos += timeSeparatorLength;
512
513        // Second
514        temp = seconds / 10;
515        buffer[pos++] = ((char) (temp + '0'));
516        buffer[pos++] = ((char) (seconds - 10 * temp + '0'));
517        buffer[pos] = millisSeparatorChar;
518        pos += millisSeparatorLength;
519
520        // Millisecond
521        temp = ms / 100;
522        buffer[pos++] = ((char) (temp + '0'));
523
524        ms -= 100 * temp;
525        temp = ms / 10;
526        buffer[pos++] = ((char) (temp + '0'));
527
528        ms -= 10 * temp;
529        buffer[pos++] = ((char) (ms + '0'));
530        return pos;
531    }
532
533    static int[] TABLE = {
534            100000, // 0
535            10000, // 1
536            1000, // 2
537            100, // 3
538            10, // 4
539            1, // 5
540    };
541
542    private void formatNanoOfMillisecond(int nanoOfMillisecond, final char[] buffer, int pos) {
543        int temp;
544        int remain = nanoOfMillisecond;
545        for (int i = 0; i < secondFractionDigits - FixedFormat.MILLI_FRACTION_DIGITS; i++) {
546            int divisor = TABLE[i];
547            temp = remain / divisor;
548            buffer[pos++] = ((char) (temp + '0'));
549            remain -= divisor * temp; // equivalent of remain % 10
550        }
551    }
552
553    private int daylightSavingTime(final int hourOfDay) {
554        return hourOfDay > 23 ? dstOffsets[23] : dstOffsets[hourOfDay];
555    }
556}