View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  
18  package org.apache.logging.log4j.core.util.datetime;
19  
20  import org.apache.logging.log4j.core.time.Instant;
21  
22  import java.util.Arrays;
23  import java.util.Calendar;
24  import java.util.Objects;
25  import java.util.TimeZone;
26  import java.util.concurrent.TimeUnit;
27  
28  /**
29   * Custom time formatter that trades flexibility for performance. This formatter only supports the date patterns defined
30   * in {@link FixedFormat}. For any other date patterns use {@link FastDateFormat}.
31   * <p>
32   * Related benchmarks: /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and
33   * /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
34   */
35  public class FixedDateFormat {
36  
37      /**
38       * Enumeration over the supported date/time format patterns.
39       * <p>
40       * Package protected for unit tests.
41       */
42      public enum FixedFormat {
43          /**
44           * ABSOLUTE time format: {@code "HH:mm:ss,SSS"}.
45           */
46          ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1, 3),
47          /**
48           * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,nnnnnn"}.
49           */
50          ABSOLUTE_MICROS("HH:mm:ss,nnnnnn", null, 0, ':', 1, ',', 1, 6),
51          /**
52           * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,nnnnnnnnn"}.
53           */
54          ABSOLUTE_NANOS("HH:mm:ss,nnnnnnnnn", null, 0, ':', 1, ',', 1, 9),
55  
56          /**
57           * ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}.
58           */
59          ABSOLUTE_PERIOD("HH:mm:ss.SSS", null, 0, ':', 1, '.', 1, 3),
60  
61          /**
62           * COMPACT time format: {@code "yyyyMMddHHmmssSSS"}.
63           */
64          COMPACT("yyyyMMddHHmmssSSS", "yyyyMMdd", 0, ' ', 0, ' ', 0, 3),
65  
66          /**
67           * DATE_AND_TIME time format: {@code "dd MMM yyyy HH:mm:ss,SSS"}.
68           */
69          DATE("dd MMM yyyy HH:mm:ss,SSS", "dd MMM yyyy ", 0, ':', 1, ',', 1, 3),
70  
71          /**
72           * DATE_AND_TIME time format variation with period separator: {@code "dd MMM yyyy HH:mm:ss.SSS"}.
73           */
74          DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS", "dd MMM yyyy ", 0, ':', 1, '.', 1, 3),
75  
76          /**
77           * DEFAULT time format: {@code "yyyy-MM-dd HH:mm:ss,SSS"}.
78           */
79          DEFAULT("yyyy-MM-dd HH:mm:ss,SSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 3),
80          /**
81           * DEFAULT time format with microsecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnn"}.
82           */
83          DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,nnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 6),
84          /**
85           * DEFAULT time format with nanosecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"}.
86           */
87          DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,nnnnnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 9),
88  
89          /**
90           * DEFAULT time format variation with period separator: {@code "yyyy-MM-dd HH:mm:ss.SSS"}.
91           */
92          DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS", "yyyy-MM-dd ", 0, ':', 1, '.', 1, 3),
93  
94          /**
95           * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss,SSS"}.
96           */
97          ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS", "yyyyMMdd'T'", 2, ' ', 0, ',', 1, 3),
98  
99          /**
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 }