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 static 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        private FixedFormat(final String pattern, final String datePattern, final int escapeCount,
091                final char timeSeparator, 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 int dateLength;
166    private final FastDateFormat fastDateFormat; // may be null
167    private final char timeSeparatorChar;
168    private final char millisSeparatorChar;
169    private final int timeSeparatorLength;
170    private final int millisSeparatorLength;
171
172    private volatile long midnightToday = 0;
173    private volatile long midnightTomorrow = 0;
174    // cachedDate does not need to be volatile because
175    // there is a write to a volatile field *after* cachedDate is modified,
176    // and there is a read from a volatile field *before* cachedDate is read.
177    // The Java memory model guarantees that because of the above,
178    // changes to cachedDate in one thread are visible to other threads.
179    // See http://g.oswego.edu/dl/jmm/cookbook.html
180    private char[] cachedDate; // may be null
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.dateLength = fixedFormat.getDatePatternLength();
197        this.fastDateFormat = fixedFormat.getFastDateFormat();
198    }
199
200    public static FixedDateFormat createIfSupported(final String... options) {
201        if (options == null || options.length == 0 || options[0] == null) {
202            return new FixedDateFormat(FixedFormat.DEFAULT);
203        }
204        if (options.length > 1) {
205            return null; // time zone not supported
206        }
207        final FixedFormat type = FixedFormat.lookup(options[0]);
208        return type == null ? null : new FixedDateFormat(type);
209    }
210
211    /**
212     * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and a {@code null} TimeZone.
213     * 
214     * @param format the format to use
215     * @return a new {@code FixedDateFormat} object
216     */
217    public static FixedDateFormat create(FixedFormat format) {
218        return new FixedDateFormat(format);
219    }
220
221    /**
222     * Returns the full pattern of the selected fixed format.
223     * 
224     * @return the full date-time pattern
225     */
226    public String getFormat() {
227        return fixedFormat.getPattern();
228    }
229
230    // Profiling showed this method is important to log4j performance. Modify with care!
231    // 30 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
232    private long millisSinceMidnight(final long now) {
233        if (now >= midnightTomorrow || now < midnightToday) {
234            updateMidnightMillis(now);
235        }
236        return now - midnightToday;
237    }
238
239    private void updateMidnightMillis(final long now) {
240
241        updateCachedDate(now);
242
243        midnightToday = calcMidnightMillis(now, 0);
244        midnightTomorrow = calcMidnightMillis(now, 1);
245    }
246
247    static long calcMidnightMillis(final long time, final int addDays) {
248        final Calendar cal = Calendar.getInstance();
249        cal.setTimeInMillis(time);
250        cal.set(Calendar.HOUR_OF_DAY, 0);
251        cal.set(Calendar.MINUTE, 0);
252        cal.set(Calendar.SECOND, 0);
253        cal.set(Calendar.MILLISECOND, 0);
254        cal.add(Calendar.DATE, addDays);
255        return cal.getTimeInMillis();
256    }
257
258    private void updateCachedDate(final long now) {
259        if (fastDateFormat != null) {
260            final StringBuilder result = fastDateFormat.format(now, new StringBuilder());
261            cachedDate = result.toString().toCharArray();
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];
269        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}