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 }