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 org.apache.logging.log4j.core.time.Instant; 021 022import java.util.Arrays; 023import java.util.Calendar; 024import java.util.Objects; 025import java.util.TimeZone; 026import java.util.concurrent.TimeUnit; 027 028/** 029 * Custom time formatter that trades flexibility for performance. This formatter only supports the date patterns defined 030 * in {@link FixedFormat}. For any other date patterns use {@link FastDateFormat}. 031 * <p> 032 * Related benchmarks: /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and 033 * /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java 034 */ 035public class FixedDateFormat { 036 037 /** 038 * Enumeration over the supported date/time format patterns. 039 * <p> 040 * Package protected for unit tests. 041 */ 042 public enum FixedFormat { 043 /** 044 * ABSOLUTE time format: {@code "HH:mm:ss,SSS"}. 045 */ 046 ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1, 3), 047 /** 048 * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,nnnnnn"}. 049 */ 050 ABSOLUTE_MICROS("HH:mm:ss,nnnnnn", null, 0, ':', 1, ',', 1, 6), 051 /** 052 * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,nnnnnnnnn"}. 053 */ 054 ABSOLUTE_NANOS("HH:mm:ss,nnnnnnnnn", null, 0, ':', 1, ',', 1, 9), 055 056 /** 057 * ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}. 058 */ 059 ABSOLUTE_PERIOD("HH:mm:ss.SSS", null, 0, ':', 1, '.', 1, 3), 060 061 /** 062 * COMPACT time format: {@code "yyyyMMddHHmmssSSS"}. 063 */ 064 COMPACT("yyyyMMddHHmmssSSS", "yyyyMMdd", 0, ' ', 0, ' ', 0, 3), 065 066 /** 067 * DATE_AND_TIME time format: {@code "dd MMM yyyy HH:mm:ss,SSS"}. 068 */ 069 DATE("dd MMM yyyy HH:mm:ss,SSS", "dd MMM yyyy ", 0, ':', 1, ',', 1, 3), 070 071 /** 072 * DATE_AND_TIME time format variation with period separator: {@code "dd MMM yyyy HH:mm:ss.SSS"}. 073 */ 074 DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS", "dd MMM yyyy ", 0, ':', 1, '.', 1, 3), 075 076 /** 077 * DEFAULT time format: {@code "yyyy-MM-dd HH:mm:ss,SSS"}. 078 */ 079 DEFAULT("yyyy-MM-dd HH:mm:ss,SSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 3), 080 /** 081 * DEFAULT time format with microsecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnn"}. 082 */ 083 DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,nnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 6), 084 /** 085 * DEFAULT time format with nanosecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"}. 086 */ 087 DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,nnnnnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 9), 088 089 /** 090 * DEFAULT time format variation with period separator: {@code "yyyy-MM-dd HH:mm:ss.SSS"}. 091 */ 092 DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS", "yyyy-MM-dd ", 0, ':', 1, '.', 1, 3), 093 094 /** 095 * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss,SSS"}. 096 */ 097 ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS", "yyyyMMdd'T'", 2, ' ', 0, ',', 1, 3), 098 099 /** 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}