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        public String getPattern() {
102            return pattern;
103        }
104
105        public String getDatePattern() {
106            return datePattern;
107        }
108
109        /**
110         * Returns the FixedFormat with the name or pattern matching the specified string or {@code null} if not found.
111         */
112        public static FixedFormat lookup(final String nameOrPattern) {
113            for (final FixedFormat type : FixedFormat.values()) {
114                if (type.name().equals(nameOrPattern) || type.getPattern().equals(nameOrPattern)) {
115                    return type;
116                }
117            }
118            return null;
119        }
120
121        public int getLength() {
122            return pattern.length() - escapeCount;
123        }
124
125        public int getDatePatternLength() {
126            return getDatePattern() == null ? 0 : getDatePattern().length() - escapeCount;
127        }
128
129        public FastDateFormat getFastDateFormat() {
130            return getDatePattern() == null ? null : FastDateFormat.getInstance(getDatePattern());
131        }
132    }
133
134    public static FixedDateFormat createIfSupported(final String... options) {
135        if (options == null || options.length == 0 || options[0] == null) {
136            return new FixedDateFormat(FixedFormat.DEFAULT);
137        }
138        if (options.length > 1) {
139            return null; // time zone not supported
140        }
141        final FixedFormat type = FixedFormat.lookup(options[0]);
142        return type == null ? null : new FixedDateFormat(type);
143    }
144
145    public static FixedDateFormat create(FixedFormat format) {
146        return new FixedDateFormat(format);
147    }
148
149    private final FixedFormat fixedFormat;
150    private final int length;
151    private final int dateLength;
152    private final FastDateFormat fastDateFormat; // may be null
153    private final char timeSeparatorChar;
154    private final char millisSeparatorChar;
155    private final int timeSeparatorLength;
156    private final int millisSeparatorLength;
157
158    private volatile long midnightToday = 0;
159    private volatile long midnightTomorrow = 0;
160    // cachedDate does not need to be volatile because
161    // there is a write to a volatile field *after* cachedDate is modified,
162    // and there is a read from a volatile field *before* cachedDate is read.
163    // The Java memory model guarantees that because of the above,
164    // changes to cachedDate in one thread are visible to other threads.
165    // See http://g.oswego.edu/dl/jmm/cookbook.html
166    private char[] cachedDate; // may be null
167
168    /**
169     * Constructs a FixedDateFormat for the specified fixed format.
170     * <p>
171     * Package protected for unit tests.
172     * 
173     * @param fixedFormat the fixed format
174     */
175    FixedDateFormat(final FixedFormat fixedFormat) {
176        this.fixedFormat = Objects.requireNonNull(fixedFormat);
177        this.timeSeparatorChar = fixedFormat.timeSeparatorChar;
178        this.timeSeparatorLength = fixedFormat.timeSeparatorLength;
179        this.millisSeparatorChar = fixedFormat.millisSeparatorChar;
180        this.millisSeparatorLength = fixedFormat.millisSeparatorLength;
181        this.length = fixedFormat.getLength();
182        this.dateLength = fixedFormat.getDatePatternLength();
183        this.fastDateFormat = fixedFormat.getFastDateFormat();
184    }
185
186    public String getFormat() {
187        return fixedFormat.getPattern();
188    }
189
190    // Profiling showed this method is important to log4j performance. Modify with care!
191    // 30 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
192    private long millisSinceMidnight(final long now) {
193        if (now >= midnightTomorrow || now < midnightToday) {
194            updateMidnightMillis(now);
195        }
196        return now - midnightToday;
197    }
198
199    private void updateMidnightMillis(final long now) {
200
201        updateCachedDate(now);
202
203        midnightToday = calcMidnightMillis(now, 0);
204        midnightTomorrow = calcMidnightMillis(now, 1);
205    }
206
207    static long calcMidnightMillis(final long time, final int addDays) {
208        final Calendar cal = Calendar.getInstance();
209        cal.setTimeInMillis(time);
210        cal.set(Calendar.HOUR_OF_DAY, 0);
211        cal.set(Calendar.MINUTE, 0);
212        cal.set(Calendar.SECOND, 0);
213        cal.set(Calendar.MILLISECOND, 0);
214        cal.add(Calendar.DATE, addDays);
215        return cal.getTimeInMillis();
216    }
217
218    private void updateCachedDate(final long now) {
219        if (fastDateFormat != null) {
220            final StringBuilder result = fastDateFormat.format(now, new StringBuilder());
221            cachedDate = result.toString().toCharArray();
222        }
223    }
224
225    // Profiling showed this method is important to log4j performance. Modify with care!
226    // 28 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
227    public String format(final long time) {
228        final char[] result = new char[length];
229        int written = format(time, result, 0);
230        return new String(result, 0, written);
231    }
232
233    // Profiling showed this method is important to log4j performance. Modify with care!
234    // 31 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
235    public int format(final long time, final char[] buffer, final int startPos) {
236        // Calculate values by getting the ms values first and do then
237        // calculate the hour minute and second values divisions.
238
239        // Get daytime in ms: this does fit into an int
240        // int ms = (int) (time % 86400000);
241        final int ms = (int) (millisSinceMidnight(time));
242        writeDate(buffer, startPos);
243        return writeTime(ms, buffer, startPos + dateLength) - startPos;
244    }
245
246    // Profiling showed this method is important to log4j performance. Modify with care!
247    // 22 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
248    private void writeDate(final char[] buffer, final int startPos) {
249        if (cachedDate != null) {
250            System.arraycopy(cachedDate, 0, buffer, startPos, dateLength);
251        }
252    }
253
254    // Profiling showed this method is important to log4j performance. Modify with care!
255    // 262 bytes (will be inlined when hot enough: <= -XX:FreqInlineSize=325 bytes on Linux)
256    private int writeTime(int ms, final char[] buffer, int pos) {
257        final int hours = ms / 3600000;
258        ms -= 3600000 * hours;
259
260        final int minutes = ms / 60000;
261        ms -= 60000 * minutes;
262
263        final int seconds = ms / 1000;
264        ms -= 1000 * seconds;
265
266        // Hour
267        int temp = hours / 10;
268        buffer[pos++] = ((char) (temp + '0'));
269
270        // Do subtract to get remainder instead of doing % 10
271        buffer[pos++] = ((char) (hours - 10 * temp + '0'));
272        buffer[pos] = timeSeparatorChar;
273        pos += timeSeparatorLength;
274
275        // Minute
276        temp = minutes / 10;
277        buffer[pos++] = ((char) (temp + '0'));
278
279        // Do subtract to get remainder instead of doing % 10
280        buffer[pos++] = ((char) (minutes - 10 * temp + '0'));
281        buffer[pos] = timeSeparatorChar;
282        pos += timeSeparatorLength;
283
284        // Second
285        temp = seconds / 10;
286        buffer[pos++] = ((char) (temp + '0'));
287        buffer[pos++] = ((char) (seconds - 10 * temp + '0'));
288        buffer[pos] = millisSeparatorChar;
289        pos += millisSeparatorLength;
290
291        // Millisecond
292        temp = ms / 100;
293        buffer[pos++] = ((char) (temp + '0'));
294
295        ms -= 100 * temp;
296        temp = ms / 10;
297        buffer[pos++] = ((char) (temp + '0'));
298
299        ms -= 10 * temp;
300        buffer[pos++] = ((char) (ms + '0'));
301        return pos;
302    }
303}