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 java.util.Arrays;
21  import java.util.Calendar;
22  import java.util.Objects;
23  import java.util.TimeZone;
24  import java.util.concurrent.TimeUnit;
25  
26  /**
27   * Custom time formatter that trades flexibility for performance. This formatter only supports the date patterns defined
28   * in {@link FixedFormat}. For any other date patterns use {@link FastDateFormat}.
29   * <p>
30   * Related benchmarks: /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and
31   * /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
32   */
33  public class FixedDateFormat {
34      /**
35       * Enumeration over the supported date/time format patterns.
36       * <p>
37       * Package protected for unit tests.
38       */
39      public enum FixedFormat {
40          /**
41           * ABSOLUTE time format: {@code "HH:mm:ss,SSS"}.
42           */
43          ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1),
44  
45          /**
46           * ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}.
47           */
48          ABSOLUTE_PERIOD("HH:mm:ss.SSS", null, 0, ':', 1, '.', 1),
49  
50          /**
51           * COMPACT time format: {@code "yyyyMMddHHmmssSSS"}.
52           */
53          COMPACT("yyyyMMddHHmmssSSS", "yyyyMMdd", 0, ' ', 0, ' ', 0),
54  
55          /**
56           * DATE_AND_TIME time format: {@code "dd MMM yyyy HH:mm:ss,SSS"}.
57           */
58          DATE("dd MMM yyyy HH:mm:ss,SSS", "dd MMM yyyy ", 0, ':', 1, ',', 1),
59  
60          /**
61           * DATE_AND_TIME time format variation with period separator: {@code "dd MMM yyyy HH:mm:ss.SSS"}.
62           */
63          DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS", "dd MMM yyyy ", 0, ':', 1, '.', 1),
64  
65          /**
66           * DEFAULT time format: {@code "yyyy-MM-dd HH:mm:ss,SSS"}.
67           */
68          DEFAULT("yyyy-MM-dd HH:mm:ss,SSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1),
69  
70          /**
71           * DEFAULT time format variation with period separator: {@code "yyyy-MM-dd HH:mm:ss.SSS"}.
72           */
73          DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS", "yyyy-MM-dd ", 0, ':', 1, '.', 1),
74  
75          /**
76           * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss,SSS"}.
77           */
78          ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS", "yyyyMMdd'T'", 2, ' ', 0, ',', 1),
79  
80          /**
81           * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss.SSS"}.
82           */
83          ISO8601_BASIC_PERIOD("yyyyMMdd'T'HHmmss.SSS", "yyyyMMdd'T'", 2, ' ', 0, '.', 1),
84  
85          /**
86           * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSS"}.
87           */
88          ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1),
89  
90          /**
91           * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss.SSS"}.
92           */
93          ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1);
94  
95          private final String pattern;
96          private final String datePattern;
97          private final int escapeCount;
98          private final char timeSeparatorChar;
99          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 }