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