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