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