You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2024/10/31 15:19:10 UTC
(logging-log4j2) 01/01: Port #3121 to `main`
This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch feature/main/instant-formatter
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit f2c7dfcbc9b1d5299d1f8da6e76397981d6c60f9
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Thu Oct 31 16:18:49 2024 +0100
Port #3121 to `main`
---
.../rolling/OnStartupTriggeringPolicyTest.java | 10 +-
...RollingAppenderDeleteAccumulatedCount1Test.java | 6 +-
...RollingAppenderDeleteAccumulatedCount2Test.java | 6 +-
.../RollingAppenderDeleteAccumulatedSizeTest.java | 6 +-
.../rolling/RollingAppenderDeleteNestedTest.java | 6 +-
.../logging/log4j/core/layout/HtmlLayoutTest.java | 69 -
.../core/pattern/DatePatternConverterTest.java | 340 +----
.../log4j/core/time/InstantFormatterTest.java | 90 --
.../time/internal/format/FixedDateFormatTest.java | 404 ------
.../instant/InstantNumberFormatterTest.java | 70 +
.../InstantPatternDynamicFormatterTest.java | 424 ++++++
...stantPatternThreadLocalCachedFormatterTest.java | 292 ++++
.../resources/log4j-rolling-size-with-time.xml | 2 +-
.../logging/log4j/core/layout/HtmlLayout.java | 2 +-
.../log4j/core/pattern/DatePatternConverter.java | 329 ++---
.../core/pattern/FileDatePatternConverter.java | 11 +-
.../logging/log4j/core/time/InstantFormatter.java | 317 -----
.../core/time/internal/format/DatePrinter.java | 144 --
.../core/time/internal/format/FastDateFormat.java | 588 --------
.../core/time/internal/format/FastDatePrinter.java | 1503 --------------------
.../core/time/internal/format/FixedDateFormat.java | 760 ----------
.../log4j/core/time/internal/format/Format.java | 31 -
.../core/time/internal/format/FormatCache.java | 265 ----
.../core/time/internal/format/package-info.java | 20 -
.../log4j/core/util/datetime/DatePrinter.java | 133 --
.../log4j/core/util/datetime/FastDateFormat.java | 207 ---
.../instant/InstantFormatter.java} | 28 +-
.../internal/instant/InstantNumberFormatter.java | 128 ++
.../instant/InstantPatternDynamicFormatter.java | 831 +++++++++++
.../internal/instant/InstantPatternFormatter.java | 166 +++
.../InstantPatternThreadLocalCachedFormatter.java | 134 ++
.../instant}/package-info.java | 18 +-
.../layout/template/json/LogEventFixture.java | 27 +-
.../log4j/layout/template/json/package-info.java | 22 -
.../template/json/resolver/TimestampResolver.java | 247 +---
.../log4j/perf/jmh/DateTimeFormatBenchmark.java | 117 --
.../perf/jmh/ThreadsafeDateFormatBenchmark.java | 241 ----
.../log4j/perf/jmh/TimeFormatBenchmark.java | 310 ----
...PatternDynamicFormatterSequencingBenchmark.java | 96 ++
.../instant/InstantPatternFormatterBenchmark.java | 151 ++
.../InstantPatternFormatterImpactBenchmark.java | 125 ++
.../.3.x.x/3149_remove_named_date_patterns.xml | 8 +
src/changelog/.index.adoc.ftl | 2 +-
.../modules/ROOT/pages/manual/pattern-layout.adoc | 77 +-
44 files changed, 2712 insertions(+), 6051 deletions(-)
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
index 0254773222..df7d585e3d 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
@@ -35,7 +35,8 @@ import java.util.stream.Stream;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.layout.PatternLayout;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -46,7 +47,8 @@ public class OnStartupTriggeringPolicyTest {
private static final String TARGET_PATTERN = "/test1-%d{MM-dd-yyyy}-%i.log";
private static final String TEST_DATA = "Hello world!";
- private static final FastDateFormat formatter = FastDateFormat.getInstance("MM-dd-yyyy");
+ private static final InstantPatternFormatter formatter =
+ InstantPatternFormatter.newBuilder().setPattern("MM-dd-yyyy").build();
@TempDir
Path tempDir;
@@ -56,7 +58,9 @@ public class OnStartupTriggeringPolicyTest {
final Configuration configuration = new DefaultConfiguration();
final Path target = tempDir.resolve("testfile");
final long timeStamp = Instant.now().minus(Duration.ofDays(1)).toEpochMilli();
- final String expectedDate = formatter.format(timeStamp);
+ final MutableInstant instant = new MutableInstant();
+ instant.initFromEpochMilli(timeStamp, 0);
+ final String expectedDate = formatter.format(instant);
final Path rolled = tempDir.resolve("test1-" + expectedDate + "-1.log");
final long copied;
try (final InputStream is = new ByteArrayInputStream(TEST_DATA.getBytes(StandardCharsets.UTF_8))) {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount1Test.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount1Test.java
index 51fead166a..423e91fd63 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount1Test.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount1Test.java
@@ -35,8 +35,6 @@ import java.util.List;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
import org.apache.logging.log4j.test.junit.CleanUpDirectories;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -76,9 +74,7 @@ public class RollingAppenderDeleteAccumulatedCount1Test {
assertTrue(files.length > 0, "Dir " + DIR + " should contain files");
for (final File file : files) {
final BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
- System.out.println(file + " (" + fileAttributes.size() + "B) "
- + FixedDateFormat.create(FixedFormat.ABSOLUTE)
- .format(fileAttributes.lastModifiedTime().toMillis()));
+ System.out.println(file + " (" + fileAttributes.size() + "B) " + fileAttributes.lastModifiedTime());
}
final List<String> expected = List.of("my-1.log", "my-2.log", "my-3.log", "my-4.log", "my-5.log");
assertEquals(expected.size() + 6, files.length, Arrays.toString(files));
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount2Test.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount2Test.java
index 0fd8772135..d0e2153585 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount2Test.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount2Test.java
@@ -35,8 +35,6 @@ import java.util.List;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
import org.apache.logging.log4j.test.junit.CleanUpDirectories;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -77,9 +75,7 @@ public class RollingAppenderDeleteAccumulatedCount2Test {
assertTrue(files.length > 0, "Dir " + DIR + " should contain files");
for (final File file : files) {
final BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
- System.out.println(file + " (" + fileAttributes.size() + "B) "
- + FixedDateFormat.create(FixedFormat.ABSOLUTE)
- .format(fileAttributes.lastModifiedTime().toMillis()));
+ System.out.println(file + " (" + fileAttributes.size() + "B) " + fileAttributes.lastModifiedTime());
}
// sometimes "test-9.log", sometimes "test-10.log" remains
final List<String> expected = List.of("my-1.log", "my-2.log", "my-3.log", "my-4.log", "my-5.log");
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedSizeTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedSizeTest.java
index 15240841f9..f0fdffc758 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedSizeTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedSizeTest.java
@@ -27,8 +27,6 @@ import java.util.Arrays;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
import org.apache.logging.log4j.test.junit.CleanUpDirectories;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -61,9 +59,7 @@ public class RollingAppenderDeleteAccumulatedSizeTest {
assertTrue(files.length > 0, "Dir " + DIR + " should contain files");
for (final File file : files) {
final BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
- System.out.println(file + " (" + fileAttributes.size() + "B) "
- + FixedDateFormat.create(FixedFormat.ABSOLUTE)
- .format(fileAttributes.lastModifiedTime().toMillis()));
+ System.out.println(file + " (" + fileAttributes.size() + "B) " + fileAttributes.lastModifiedTime());
}
assertEquals(4, files.length, Arrays.toString(files));
long total = 0;
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteNestedTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteNestedTest.java
index 38f7417a95..a8133732b0 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteNestedTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteNestedTest.java
@@ -35,8 +35,6 @@ import java.util.List;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
import org.apache.logging.log4j.test.junit.CleanUpDirectories;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -76,9 +74,7 @@ public class RollingAppenderDeleteNestedTest {
assertTrue(files.length > 0, "Dir " + DIR + " should contain files");
for (final File file : files) {
final BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
- System.out.println(file + " (" + fileAttributes.size() + "B) "
- + FixedDateFormat.create(FixedFormat.ABSOLUTE)
- .format(fileAttributes.lastModifiedTime().toMillis()));
+ System.out.println(file + " (" + fileAttributes.size() + "B) " + fileAttributes.lastModifiedTime());
}
final List<String> expected = Arrays.asList("my-1.log", "my-2.log", "my-3.log", "my-4.log", "my-5.log");
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/HtmlLayoutTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/HtmlLayoutTest.java
index cb14f727ea..bab2f93072 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/HtmlLayoutTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/HtmlLayoutTest.java
@@ -16,26 +16,19 @@
*/
package org.apache.logging.log4j.core.layout;
-import static org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets;
-import java.text.MessageFormat;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.AbstractLogEvent;
import org.apache.logging.log4j.core.Appender;
-import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
@@ -45,7 +38,6 @@ import org.apache.logging.log4j.core.test.appender.ListAppender;
import org.apache.logging.log4j.core.test.junit.ConfigurationFactoryType;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.test.junit.UsingAnyThreadContext;
@@ -243,68 +235,7 @@ public class HtmlLayoutTest {
assertEquals("<td>" + event.getTimeMillis() + "</td>", actual, "Incorrect date:" + actual);
}
- @Test
- public void testLayoutWithDatePatternFixedFormat() {
- for (final String timeZone : new String[] {"GMT+8", "GMT+0530", "UTC", null}) {
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- testLayoutWithDatePatternFixedFormat(format, timeZone);
- }
- }
- }
-
private String getDateLine(final String logEventString) {
return logEventString.split(System.lineSeparator())[2];
}
-
- private void testLayoutWithDatePatternFixedFormat(final FixedFormat format, final String timezone) {
- final HtmlLayout layout = HtmlLayout.newBuilder()
- .setConfiguration(new DefaultConfiguration())
- .setDatePattern(format.name())
- .setTimezone(timezone)
- .build();
-
- final LogEvent event = new MyLogEvent();
- final String actual = getDateLine(layout.toSerializable(event));
-
- // build expected date string
- final java.time.Instant instant = java.time.Instant.ofEpochSecond(
- event.getInstant().getEpochSecond(), event.getInstant().getNanoOfSecond());
- ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
- if (timezone != null) {
- zonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of(timezone));
- }
-
- // LOG4J2-3019 HtmlLayoutTest.testLayoutWithDatePatternFixedFormat test fails on windows
- // https://issues.apache.org/jira/browse/LOG4J2-3019
- // java.time.format.DateTimeFormatterBuilder.toFormatter() defaults to using
- // Locale.getDefault(Locale.Category.FORMAT)
- final Locale formatLocale = Locale.getDefault(Locale.Category.FORMAT);
- final Locale locale = Locale.getDefault().equals(formatLocale) ? formatLocale : Locale.getDefault();
-
- // For DateTimeFormatter of jdk,
- // Pattern letter 'S' means fraction-of-second, 'n' means nano-of-second. Log4j2 needs S.
- // Pattern letter 'X' (upper case) will output 'Z' when the offset to be output would be zero,
- // whereas pattern letter 'x' (lower case) will output '+00', '+0000', or '+00:00'. Log4j2 needs x.
- final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(
- format.getPattern().replace('n', 'S').replace('X', 'x'), locale);
- String expected = zonedDateTime.format(dateTimeFormatter);
-
- final String offset = zonedDateTime.getOffset().toString();
-
- // Truncate minutes if timeZone format is HH and timeZone has minutes. This is required because according to
- // DateTimeFormatter,
- // One letter outputs just the hour, such as '+01', unless the minute is non-zero in which case the minute is
- // also output, such as '+0130'
- // ref : https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
- if (FixedDateFormat.FixedTimeZoneFormat.HH.equals(format.getTimeZoneFormat())
- && offset.contains(":")
- && !"00".equals(offset.split(":")[1])) {
- expected = expected.substring(0, expected.length() - 2);
- }
-
- assertEquals(
- "<td>" + expected + "</td>",
- actual,
- MessageFormat.format("Incorrect date={0}, format={1}, timezone={2}", actual, format.name(), timezone));
- }
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
index d908f8013f..961a30f61a 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
@@ -19,17 +19,12 @@ package org.apache.logging.log4j.core.pattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
-import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
-import java.util.TimeZone;
import org.apache.logging.log4j.core.AbstractLogEvent;
import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.config.NullConfiguration;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedTimeZoneFormat;
import org.apache.logging.log4j.util.Strings;
import org.junit.jupiter.api.Test;
@@ -53,29 +48,7 @@ class DatePatternConverterTest {
}
}
- /**
- * SimpleTimePattern for DEFAULT.
- */
- private static final String DEFAULT_PATTERN = FixedDateFormat.FixedFormat.DEFAULT.getPattern();
-
- /**
- * ISO8601 string literal.
- */
- private static final String ISO8601 = FixedDateFormat.FixedFormat.ISO8601.name();
-
- /**
- * ISO8601_OFFSE_DATE_TIME_XX string literal.
- */
- private static final String ISO8601_OFFSE_DATE_TIME_HHMM =
- FixedDateFormat.FixedFormat.ISO8601_OFFSET_DATE_TIME_HHMM.name();
-
- /**
- * ISO8601_OFFSET_DATE_TIME_XXX string literal.
- */
- private static final String ISO8601_OFFSET_DATE_TIME_HHCMM =
- FixedDateFormat.FixedFormat.ISO8601_OFFSET_DATE_TIME_HHCMM.name();
-
- private static final String[] ISO8601_FORMAT_OPTIONS = {ISO8601};
+ private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
private static Date date(final int year, final int month, final int date) {
final Calendar cal = Calendar.getInstance();
@@ -84,55 +57,9 @@ class DatePatternConverterTest {
return cal.getTime();
}
- private String precisePattern(final String pattern, final int precision) {
- final String search = "SSS";
- final int foundIndex = pattern.indexOf(search);
- final String seconds = pattern.substring(0, foundIndex);
- final String remainder = pattern.substring(foundIndex + search.length());
- return seconds + "nnnnnnnnn".substring(0, precision) + remainder;
- }
-
@Test
public void testFormatDateStringBuilderDefaultPattern() {
- assertDatePattern(null, date(2001, 1, 1), "2001-02-01 14:15:16,123");
- }
-
- @Test
- public void testFormatDateStringBuilderIso8601() {
- final DatePatternConverter converter = createConverter(ISO8601_FORMAT_OPTIONS);
- final StringBuilder sb = new StringBuilder();
- converter.format(date(2001, 1, 1), sb);
-
- final String expected = "2001-02-01T14:15:16,123";
- assertEquals(expected, sb.toString());
- }
-
- @Test
- public void testFormatDateStringBuilderIso8601BasicWithPeriod() {
- assertDatePattern(
- FixedDateFormat.FixedFormat.ISO8601_BASIC_PERIOD.name(), date(2001, 1, 1), "20010201T141516.123");
- }
-
- @Test
- public void testFormatDateStringBuilderIso8601WithPeriod() {
- assertDatePattern(
- FixedDateFormat.FixedFormat.ISO8601_PERIOD.name(), date(2001, 1, 1), "2001-02-01T14:15:16.123");
- }
-
- @Test
- public void testFormatDateStringBuilderIso8601WithPeriodMicroseconds() {
- final String[] pattern = {FixedDateFormat.FixedFormat.ISO8601_PERIOD_MICROS.name(), "Z"};
- final DatePatternConverter converter = createConverter(pattern);
- final StringBuilder sb = new StringBuilder();
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochMilli(
- 1577225134559L,
- // One microsecond
- 1000);
- converter.format(instant, sb);
-
- final String expected = "2019-12-24T22:05:34.559001";
- assertEquals(expected, sb.toString());
+ assertDatePattern(null, date(2001, 1, 1), "2001-02-01 14:15:16.123");
}
@Test
@@ -143,126 +70,34 @@ class DatePatternConverterTest {
@Test
public void testFormatLogEventStringBuilderDefaultPattern() {
final LogEvent event = new MyLogEvent();
- final DatePatternConverter converter = createConverter(null);
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
-
- final String expected = "2011-12-30 10:56:35,987";
- assertEquals(expected, sb.toString());
- }
-
- @Test
- public void testFormatLogEventStringBuilderIso8601() {
- final LogEvent event = new MyLogEvent();
- final DatePatternConverter converter = createConverter(ISO8601_FORMAT_OPTIONS);
+ final DatePatternConverter converter = DatePatternConverter.newInstance(null);
final StringBuilder sb = new StringBuilder();
converter.format(event, sb);
- final String expected = "2011-12-30T10:56:35,987";
+ final String expected = "2011-12-30 10:56:35.987";
assertEquals(expected, sb.toString());
}
@Test
public void testFormatAmericanPatterns() {
final Date date = date(2011, 2, 11);
- assertDatePattern("US_MONTH_DAY_YEAR4_TIME", date, "11/03/2011 14:15:16.123");
- assertDatePattern("US_MONTH_DAY_YEAR2_TIME", date, "11/03/11 14:15:16.123");
assertDatePattern("dd/MM/yyyy HH:mm:ss.SSS", date, "11/03/2011 14:15:16.123");
- assertDatePattern("dd/MM/yyyy HH:mm:ss.nnnnnn", date, "11/03/2011 14:15:16.123000");
+ assertDatePattern("dd/MM/yyyy HH:mm:ss.SSSSSS", date, "11/03/2011 14:15:16.123000");
assertDatePattern("dd/MM/yy HH:mm:ss.SSS", date, "11/03/11 14:15:16.123");
- assertDatePattern("dd/MM/yy HH:mm:ss.nnnnnn", date, "11/03/11 14:15:16.123000");
+ assertDatePattern("dd/MM/yy HH:mm:ss.SSSSSS", date, "11/03/11 14:15:16.123000");
}
private static void assertDatePattern(final String format, final Date date, final String expected) {
- final DatePatternConverter converter = createConverter(new String[] {format});
+ final DatePatternConverter converter = DatePatternConverter.newInstance(new String[] {format});
final StringBuilder sb = new StringBuilder();
converter.format(date, sb);
assertEquals(expected, sb.toString());
}
- @Test
- public void testFormatLogEventStringBuilderIso8601TimezoneJST() {
- final LogEvent event = new MyLogEvent();
- final String[] optionsWithTimezone = {ISO8601, "JST"};
- final DatePatternConverter converter = createConverter(optionsWithTimezone);
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
-
- // JST=Japan Standard Time: UTC+9:00
- final TimeZone tz = TimeZone.getTimeZone("JST");
- final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
- sdf.setTimeZone(tz);
- final long adjusted = event.getTimeMillis() + tz.getDSTSavings();
- final String expected = sdf.format(new Date(adjusted));
- // final String expected = "2011-12-30T18:56:35,987"; // in CET (Central Eastern Time: Amsterdam)
- assertEquals(expected, sb.toString());
- }
-
- @Test
- public void testFormatLogEventStringBuilderIso8601TimezoneOffsetHHCMM() {
- final LogEvent event = new MyLogEvent();
- final String[] optionsWithTimezone = {ISO8601_OFFSET_DATE_TIME_HHCMM};
- final DatePatternConverter converter = createConverter(optionsWithTimezone);
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
-
- final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
- final String format = sdf.format(new Date(event.getTimeMillis()));
- final String expected = format.endsWith("Z") ? format.substring(0, format.length() - 1) + "+00:00" : format;
- assertEquals(expected, sb.toString());
- }
-
- @Test
- public void testFormatLogEventStringBuilderIso8601TimezoneOffsetHHMM() {
- final LogEvent event = new MyLogEvent();
- final String[] optionsWithTimezone = {ISO8601_OFFSE_DATE_TIME_HHMM};
- final DatePatternConverter converter = createConverter(optionsWithTimezone);
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
-
- final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
- final String format = sdf.format(new Date(event.getTimeMillis()));
- final String expected = format.endsWith("Z") ? format.substring(0, format.length() - 1) + "+0000" : format;
- assertEquals(expected, sb.toString());
- }
-
- @Test
- public void testFormatLogEventStringBuilderIso8601TimezoneUTC() {
- final LogEvent event = new MyLogEvent();
- final DatePatternConverter converter = createConverter(new String[] {"ISO8601", "UTC"});
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
-
- final TimeZone tz = TimeZone.getTimeZone("UTC");
- final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
- sdf.setTimeZone(tz);
- final long adjusted = event.getTimeMillis() + tz.getDSTSavings();
- final String expected = sdf.format(new Date(adjusted));
- // final String expected = "2011-12-30T09:56:35,987";
- assertEquals(expected, sb.toString());
- }
-
- @Test
- public void testFormatLogEventStringBuilderIso8601TimezoneZ() {
- final LogEvent event = new MyLogEvent();
- final String[] optionsWithTimezone = {ISO8601, "Z"};
- final DatePatternConverter converter = createConverter(optionsWithTimezone);
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
-
- final TimeZone tz = TimeZone.getTimeZone("UTC");
- final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
- sdf.setTimeZone(tz);
- final long adjusted = event.getTimeMillis() + tz.getDSTSavings();
- final String expected = sdf.format(new Date(adjusted));
- // final String expected = "2011-12-30T17:56:35,987"; // in UTC
- assertEquals(expected, sb.toString());
- }
-
@Test
public void testFormatObjectStringBuilderDefaultPattern() {
- final DatePatternConverter converter = createConverter(null);
+ final DatePatternConverter converter = DatePatternConverter.newInstance(null);
final StringBuilder sb = new StringBuilder();
converter.format("nondate", sb);
@@ -272,187 +107,58 @@ class DatePatternConverterTest {
@Test
public void testFormatStringBuilderObjectArrayDefaultPattern() {
- final DatePatternConverter converter = createConverter(null);
+ final DatePatternConverter converter = DatePatternConverter.newInstance(null);
final StringBuilder sb = new StringBuilder();
converter.format(sb, date(2001, 1, 1), date(2002, 2, 2), date(2003, 3, 3));
- final String expected = "2001-02-01 14:15:16,123"; // only process first date
- assertEquals(expected, sb.toString());
- }
-
- @Test
- public void testFormatStringBuilderObjectArrayIso8601() {
- final DatePatternConverter converter = createConverter(ISO8601_FORMAT_OPTIONS);
- final StringBuilder sb = new StringBuilder();
- converter.format(sb, date(2001, 1, 1), date(2002, 2, 2), date(2003, 3, 3));
-
- final String expected = "2001-02-01T14:15:16,123"; // only process first date
+ final String expected = "2001-02-01 14:15:16.123"; // only process first date
assertEquals(expected, sb.toString());
}
@Test
public void testGetPatternReturnsDefaultForEmptyOptionsArray() {
- assertEquals(DEFAULT_PATTERN, createConverter(new String[0]).getPattern());
+ assertEquals(
+ DEFAULT_PATTERN,
+ DatePatternConverter.newInstance(Strings.EMPTY_ARRAY).getPattern());
}
@Test
public void testGetPatternReturnsDefaultForInvalidPattern() {
- final String[] invalid = {"ABC I am not a valid date pattern"};
- assertEquals(DEFAULT_PATTERN, createConverter(invalid).getPattern());
+ final String[] invalid = {"A single `V` is not allow by `DateTimeFormatter` and should cause an exception"};
+ assertEquals(DEFAULT_PATTERN, DatePatternConverter.newInstance(invalid).getPattern());
}
@Test
public void testGetPatternReturnsDefaultForNullOptions() {
- assertEquals(DEFAULT_PATTERN, createConverter(null).getPattern());
+ assertEquals(DEFAULT_PATTERN, DatePatternConverter.newInstance(null).getPattern());
}
@Test
public void testGetPatternReturnsDefaultForSingleNullElementOptionsArray() {
- assertEquals(DEFAULT_PATTERN, createConverter(new String[1]).getPattern());
+ assertEquals(
+ DEFAULT_PATTERN, DatePatternConverter.newInstance(new String[1]).getPattern());
}
@Test
public void testGetPatternReturnsDefaultForTwoNullElementsOptionsArray() {
- assertEquals(DEFAULT_PATTERN, createConverter(new String[2]).getPattern());
+ assertEquals(
+ DEFAULT_PATTERN, DatePatternConverter.newInstance(new String[2]).getPattern());
}
@Test
public void testGetPatternReturnsNullForUnix() {
final String[] options = {"UNIX"};
- assertNull(createConverter(options).getPattern());
+ assertNull(DatePatternConverter.newInstance(options).getPattern());
}
@Test
public void testGetPatternReturnsNullForUnixMillis() {
final String[] options = {"UNIX_MILLIS"};
- assertNull(createConverter(options).getPattern());
- }
-
- @Test
- public void testInvalidLongPatternIgnoresExcessiveDigits() {
- final StringBuilder preciseBuilder = new StringBuilder();
- final StringBuilder milliBuilder = new StringBuilder();
- final LogEvent event = new MyLogEvent();
-
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- final String pattern = format.getPattern();
- final String search = "SSS";
- final int foundIndex = pattern.indexOf(search);
- if (pattern.endsWith("n") || pattern.matches(".+n+X*") || pattern.matches(".+n+Z*")) {
- // ignore patterns that already have precise time formats
- // ignore patterns that do not use seconds.
- continue;
- }
- preciseBuilder.setLength(0);
- milliBuilder.setLength(0);
-
- final DatePatternConverter preciseConverter;
- final String precisePattern;
- if (foundIndex < 0) {
- precisePattern = pattern;
- preciseConverter = createConverter(new String[] {precisePattern});
- } else {
- final String subPattern = pattern.substring(0, foundIndex);
- final String remainder = pattern.substring(foundIndex + search.length());
- precisePattern = subPattern + "nnnnnnnnn" + "n" + remainder; // nanos too long
- preciseConverter = createConverter(new String[] {precisePattern});
- }
- preciseConverter.format(event, preciseBuilder);
-
- final String[] milliOptions = {pattern};
- createConverter(milliOptions).format(event, milliBuilder);
- final FixedTimeZoneFormat timeZoneFormat = format.getTimeZoneFormat();
- final int truncateLen = 3 + (timeZoneFormat != null ? timeZoneFormat.getLength() : 0);
- final String tz = timeZoneFormat != null
- ? milliBuilder.substring(milliBuilder.length() - timeZoneFormat.getLength(), milliBuilder.length())
- : Strings.EMPTY;
- milliBuilder.setLength(milliBuilder.length() - truncateLen); // truncate millis
- if (foundIndex >= 0) {
- milliBuilder.append("987123456");
- }
- final String expected = milliBuilder.append(tz).toString();
-
- assertEquals(
- expected,
- preciseBuilder.toString(),
- "format = " + format + ", pattern = " + pattern + ", precisePattern = " + precisePattern);
- // System.out.println(preciseOptions[0] + ": " + precise);
- }
+ assertNull(DatePatternConverter.newInstance(options).getPattern());
}
@Test
public void testNewInstanceAllowsNullParameter() {
- createConverter(null); // no errors
- }
-
- // test with all formats from one 'n' (100s of millis) to 'nnnnnnnnn' (nanosecond precision)
- @Test
- public void testPredefinedFormatWithAnyValidNanoPrecision() {
- final StringBuilder preciseBuilder = new StringBuilder();
- final StringBuilder milliBuilder = new StringBuilder();
- final LogEvent event = new MyLogEvent();
-
- for (final String timeZone : new String[] {"PST", null}) { // Pacific Standard Time=UTC-8:00
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- for (int i = 1; i <= 9; i++) {
- final String pattern = format.getPattern();
- if (pattern.endsWith("n")
- || pattern.matches(".+n+X*")
- || pattern.matches(".+n+Z*")
- || !pattern.contains("SSS")) {
- // ignore patterns that already have precise time formats
- // ignore patterns that do not use seconds.
- continue;
- }
- preciseBuilder.setLength(0);
- milliBuilder.setLength(0);
-
- final String precisePattern = precisePattern(pattern, i);
- final String[] preciseOptions = {precisePattern, timeZone};
- final DatePatternConverter preciseConverter = createConverter(preciseOptions);
- preciseConverter.format(event, preciseBuilder);
-
- final String[] milliOptions = {pattern, timeZone};
- createConverter(milliOptions).format(event, milliBuilder);
- final FixedTimeZoneFormat timeZoneFormat = format.getTimeZoneFormat();
- final int truncateLen = 3 + (timeZoneFormat != null ? timeZoneFormat.getLength() : 0);
- final String tz = timeZoneFormat != null
- ? milliBuilder.substring(
- milliBuilder.length() - timeZoneFormat.getLength(), milliBuilder.length())
- : Strings.EMPTY;
- milliBuilder.setLength(milliBuilder.length() - truncateLen); // truncate millis
- final String expected =
- milliBuilder.append("987123456", 0, i).append(tz).toString();
-
- assertEquals(
- expected,
- preciseBuilder.toString(),
- "format = " + format + ", pattern = " + pattern + ", precisePattern = " + precisePattern);
- // System.out.println(preciseOptions[0] + ": " + precise);
- }
- }
- }
- }
-
- @Test
- public void testPredefinedFormatWithoutTimezone() {
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- final String[] options = {format.name()};
- final DatePatternConverter converter = createConverter(options);
- assertEquals(format.getPattern(), converter.getPattern());
- }
- }
-
- @Test
- public void testPredefinedFormatWithTimezone() {
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- final String[] options = {format.name(), "PST"}; // Pacific Standard Time=UTC-8:00
- final DatePatternConverter converter = createConverter(options);
- assertEquals(format.getPattern(), converter.getPattern());
- }
- }
-
- private static DatePatternConverter createConverter(final String[] options) {
- return DatePatternConverter.newInstance(new NullConfiguration(), options);
+ DatePatternConverter.newInstance(null); // no errors
}
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/InstantFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/InstantFormatterTest.java
deleted file mode 100644
index 783e438078..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/InstantFormatterTest.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time;
-
-import java.util.Locale;
-import java.util.TimeZone;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.assertj.core.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
-
-class InstantFormatterTest {
-
- @ParameterizedTest
- @CsvSource({
- "yyyy-MM-dd'T'HH:mm:ss.SSS" + ",FixedDateFormat",
- "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + ",FastDateFormat",
- "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'" + ",DateTimeFormatter"
- })
- void all_internal_implementations_should_be_used(final String pattern, final String className) {
- final InstantFormatter formatter =
- InstantFormatter.newBuilder().setPattern(pattern).build();
- Assertions.assertThat(formatter.getInternalImplementationClass())
- .asString()
- .describedAs("pattern=%s", pattern)
- .endsWith("." + className);
- }
-
- @Test
- void nanoseconds_should_be_formatted() {
- final InstantFormatter formatter = InstantFormatter.newBuilder()
- .setPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'")
- .setTimeZone(TimeZone.getTimeZone("UTC"))
- .build();
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(0, 123_456_789);
- Assertions.assertThat(formatter.format(instant)).isEqualTo("1970-01-01T00:00:00.123456789Z");
- }
-
- /**
- * Reproduces <a href="https://issues.apache.org/jira/browse/LOG4J2-3614">LOG4J2-3614</a>.
- */
- @Test
- void FastDateFormat_failures_should_be_handled() {
-
- // Define a pattern causing `FastDateFormat` to fail.
- final String pattern = "ss.nnnnnnnnn";
- final TimeZone timeZone = TimeZone.getTimeZone("UTC");
- final Locale locale = Locale.US;
-
- // Assert that the pattern is not supported by `FixedDateFormat`.
- final FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(pattern, timeZone.getID());
- Assertions.assertThat(fixedDateFormat).isNull();
-
- // Assert that the pattern indeed causes a `FastDateFormat` failure.
- Assertions.assertThatThrownBy(() -> FastDateFormat.getInstance(pattern, timeZone, locale))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Illegal pattern component: nnnnnnnnn");
-
- // Assert that `InstantFormatter` falls back to `DateTimeFormatter`.
- final InstantFormatter formatter = InstantFormatter.newBuilder()
- .setPattern(pattern)
- .setTimeZone(timeZone)
- .build();
- Assertions.assertThat(formatter.getInternalImplementationClass())
- .asString()
- .endsWith(".DateTimeFormatter");
-
- // Assert that formatting works.
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(0, 123_456_789);
- Assertions.assertThat(formatter.format(instant)).isEqualTo("00.123456789");
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java
deleted file mode 100644
index 691483c029..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java
+++ /dev/null
@@ -1,404 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time.internal.format;
-
-import static org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat.DEFAULT;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
-import org.junit.Test;
-
-/**
- * Tests {@link FixedDateFormat}.
- */
-public class FixedDateFormatTest {
-
- private boolean containsNanos(final FixedFormat fixedFormat) {
- final String pattern = fixedFormat.getPattern();
- return pattern.endsWith("n") || pattern.matches(".+n+X*") || pattern.matches(".+n+Z*");
- }
-
- @Test
- public void testFixedFormat_getDatePatternNullIfNoDateInPattern() {
- assertNull(FixedFormat.ABSOLUTE.getDatePattern());
- assertNull(FixedFormat.ABSOLUTE_PERIOD.getDatePattern());
- }
-
- @Test
- public void testFixedFormat_getDatePatternLengthZeroIfNoDateInPattern() {
- assertEquals(0, FixedFormat.ABSOLUTE.getDatePatternLength());
- assertEquals(0, FixedFormat.ABSOLUTE_PERIOD.getDatePatternLength());
- }
-
- @Test
- public void testFixedFormat_getFastDateFormatNullIfNoDateInPattern() {
- assertNull(FixedFormat.ABSOLUTE.getFastDateFormat());
- assertNull(FixedFormat.ABSOLUTE_PERIOD.getFastDateFormat());
- }
-
- @Test
- public void testFixedFormat_getDatePatternReturnsDatePatternIfExists() {
- assertEquals("yyyyMMdd", FixedFormat.COMPACT.getDatePattern());
- assertEquals("yyyy-MM-dd ", DEFAULT.getDatePattern());
- }
-
- @Test
- public void testFixedFormat_getDatePatternLengthReturnsDatePatternLength() {
- assertEquals("yyyyMMdd".length(), FixedFormat.COMPACT.getDatePatternLength());
- assertEquals("yyyy-MM-dd ".length(), DEFAULT.getDatePatternLength());
- }
-
- @Test
- public void testFixedFormat_getFastDateFormatNonNullIfDateInPattern() {
- assertNotNull(FixedFormat.COMPACT.getFastDateFormat());
- assertNotNull(DEFAULT.getFastDateFormat());
- assertEquals("yyyyMMdd", FixedFormat.COMPACT.getFastDateFormat().getPattern());
- assertEquals("yyyy-MM-dd ", DEFAULT.getFastDateFormat().getPattern());
- }
-
- @Test
- public void testCreateIfSupported_nonNullIfNameMatches() {
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- final String[] options = {format.name()};
- assertNotNull(format.name(), FixedDateFormat.createIfSupported(options));
- }
- }
-
- @Test
- public void testCreateIfSupported_nonNullIfPatternMatches() {
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- final String[] options = {format.getPattern()};
- assertNotNull(format.name(), FixedDateFormat.createIfSupported(options));
- }
- }
-
- @Test
- public void testCreateIfSupported_nullIfNameDoesNotMatch() {
- final String[] options = {"DEFAULT3"};
- assertNull("DEFAULT3", FixedDateFormat.createIfSupported(options));
- }
-
- @Test
- public void testCreateIfSupported_nullIfPatternDoesNotMatch() {
- final String[] options = {"y M d H m s"};
- assertNull("y M d H m s", FixedDateFormat.createIfSupported(options));
- }
-
- @Test
- public void testCreateIfSupported_defaultIfOptionsArrayNull() {
- final FixedDateFormat fmt = FixedDateFormat.createIfSupported((String[]) null);
- assertEquals(DEFAULT.getPattern(), fmt.getFormat());
- }
-
- @Test
- public void testCreateIfSupported_defaultIfOptionsArrayEmpty() {
- final FixedDateFormat fmt = FixedDateFormat.createIfSupported(new String[0]);
- assertEquals(DEFAULT.getPattern(), fmt.getFormat());
- }
-
- @Test
- public void testCreateIfSupported_defaultIfOptionsArrayWithSingleNullElement() {
- final FixedDateFormat fmt = FixedDateFormat.createIfSupported(new String[1]);
- assertEquals(DEFAULT.getPattern(), fmt.getFormat());
- assertEquals(TimeZone.getDefault(), fmt.getTimeZone());
- }
-
- @Test
- public void testCreateIfSupported_defaultTimeZoneIfOptionsArrayWithSecondNullElement() {
- final FixedDateFormat fmt = FixedDateFormat.createIfSupported(new String[] {DEFAULT.getPattern(), null, ""});
- assertEquals(DEFAULT.getPattern(), fmt.getFormat());
- assertEquals(TimeZone.getDefault(), fmt.getTimeZone());
- }
-
- @Test
- public void testCreateIfSupported_customTimeZoneIfOptionsArrayWithTimeZoneElement() {
- final FixedDateFormat fmt =
- FixedDateFormat.createIfSupported(new String[] {DEFAULT.getPattern(), "+08:00", ""});
- assertEquals(DEFAULT.getPattern(), fmt.getFormat());
- assertEquals(TimeZone.getTimeZone("GMT+08:00"), fmt.getTimeZone());
- }
-
- @Test(expected = NullPointerException.class)
- public void testConstructorDisallowsNullFormat() {
- new FixedDateFormat(null, TimeZone.getDefault());
- }
-
- @Test(expected = NullPointerException.class)
- public void testConstructorDisallowsNullTimeZone() {
- new FixedDateFormat(FixedFormat.ABSOLUTE, null);
- }
-
- @Test
- public void testGetFormatReturnsConstructorFixedFormatPattern() {
- final FixedDateFormat format = new FixedDateFormat(FixedDateFormat.FixedFormat.ABSOLUTE, TimeZone.getDefault());
- assertSame(FixedDateFormat.FixedFormat.ABSOLUTE.getPattern(), format.getFormat());
- }
-
- @Test
- public void testFormatLong() {
- final long now = System.currentTimeMillis();
- final long start = now - TimeUnit.HOURS.toMillis(25);
- final long end = now + TimeUnit.HOURS.toMillis(25);
- for (final FixedFormat format : FixedFormat.values()) {
- final String pattern = format.getPattern();
- if (containsNanos(format) || format.getTimeZoneFormat() != null) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
- }
- final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
- final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
- for (long time = start; time < end; time += 12345) {
- final String actual = customTF.format(time);
- final String expected = simpleDF.format(new Date(time));
- assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
- }
- }
- }
-
- @Test
- public void testFormatLong_goingBackInTime() {
- final long now = System.currentTimeMillis();
- final long start = now - TimeUnit.HOURS.toMillis(25);
- final long end = now + TimeUnit.HOURS.toMillis(25);
- for (final FixedFormat format : FixedFormat.values()) {
- final String pattern = format.getPattern();
- if (containsNanos(format) || format.getTimeZoneFormat() != null) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
- }
- final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
- final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
- for (long time = end; time > start; time -= 12345) {
- final String actual = customTF.format(time);
- final String expected = simpleDF.format(new Date(time));
- assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
- }
- }
- }
-
- @Test
- public void testFormatLongCharArrayInt() {
- final long now = System.currentTimeMillis();
- final long start = now - TimeUnit.HOURS.toMillis(25);
- final long end = now + TimeUnit.HOURS.toMillis(25);
- final char[] buffer = new char[128];
- for (final FixedFormat format : FixedFormat.values()) {
- final String pattern = format.getPattern();
- if (containsNanos(format) || format.getTimeZoneFormat() != null) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
- }
- final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
- final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
- for (long time = start; time < end; time += 12345) {
- final int length = customTF.format(time, buffer, 23);
- final String actual = new String(buffer, 23, length);
- final String expected = simpleDF.format(new Date(time));
- assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
- }
- }
- }
-
- @Test
- public void testFormatLongCharArrayInt_goingBackInTime() {
- final long now = System.currentTimeMillis();
- final long start = now - TimeUnit.HOURS.toMillis(25);
- final long end = now + TimeUnit.HOURS.toMillis(25);
- final char[] buffer = new char[128];
- for (final FixedFormat format : FixedFormat.values()) {
- final String pattern = format.getPattern();
- if (containsNanos(format) || format.getTimeZoneFormat() != null) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
- }
- final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
- final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
- for (long time = end; time > start; time -= 12345) {
- final int length = customTF.format(time, buffer, 23);
- final String actual = new String(buffer, 23, length);
- final String expected = simpleDF.format(new Date(time));
- assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
- }
- }
- }
-
- @Test
- public void testDaylightSavingToSummerTime() throws Exception {
- final Calendar calendar = Calendar.getInstance();
- calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse("2017-03-12 00:00:00 UTC"));
-
- final SimpleDateFormat usCentral = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS", Locale.US);
- usCentral.setTimeZone(TimeZone.getTimeZone("US/Central"));
-
- final SimpleDateFormat utc = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS", Locale.US);
- utc.setTimeZone(TimeZone.getTimeZone("UTC"));
-
- final FixedDateFormat fixedUsCentral = FixedDateFormat.create(DEFAULT, TimeZone.getTimeZone("US/Central"));
- final FixedDateFormat fixedUtc = FixedDateFormat.create(DEFAULT, TimeZone.getTimeZone("UTC"));
-
- final String[][] expectedDstAndNoDst = {
- // US/Central, UTC
- {"2017-03-11 18:00:00,000", "2017-03-12 00:00:00,000"}, //
- {"2017-03-11 19:00:00,000", "2017-03-12 01:00:00,000"}, //
- {"2017-03-11 20:00:00,000", "2017-03-12 02:00:00,000"}, //
- {"2017-03-11 21:00:00,000", "2017-03-12 03:00:00,000"}, //
- {"2017-03-11 22:00:00,000", "2017-03-12 04:00:00,000"}, //
- {"2017-03-11 23:00:00,000", "2017-03-12 05:00:00,000"}, //
- {"2017-03-12 00:00:00,000", "2017-03-12 06:00:00,000"}, //
- {"2017-03-12 01:00:00,000", "2017-03-12 07:00:00,000"}, //
- {"2017-03-12 03:00:00,000", "2017-03-12 08:00:00,000"}, // DST jump at 2am US central time
- {"2017-03-12 04:00:00,000", "2017-03-12 09:00:00,000"}, //
- {"2017-03-12 05:00:00,000", "2017-03-12 10:00:00,000"}, //
- {"2017-03-12 06:00:00,000", "2017-03-12 11:00:00,000"}, //
- {"2017-03-12 07:00:00,000", "2017-03-12 12:00:00,000"}, //
- {"2017-03-12 08:00:00,000", "2017-03-12 13:00:00,000"}, //
- {"2017-03-12 09:00:00,000", "2017-03-12 14:00:00,000"}, //
- {"2017-03-12 10:00:00,000", "2017-03-12 15:00:00,000"}, //
- {"2017-03-12 11:00:00,000", "2017-03-12 16:00:00,000"}, //
- {"2017-03-12 12:00:00,000", "2017-03-12 17:00:00,000"}, //
- {"2017-03-12 13:00:00,000", "2017-03-12 18:00:00,000"}, //
- {"2017-03-12 14:00:00,000", "2017-03-12 19:00:00,000"}, //
- {"2017-03-12 15:00:00,000", "2017-03-12 20:00:00,000"}, //
- {"2017-03-12 16:00:00,000", "2017-03-12 21:00:00,000"}, //
- {"2017-03-12 17:00:00,000", "2017-03-12 22:00:00,000"}, //
- {"2017-03-12 18:00:00,000", "2017-03-12 23:00:00,000"}, // 24
- {"2017-03-12 19:00:00,000", "2017-03-13 00:00:00,000"}, //
- {"2017-03-12 20:00:00,000", "2017-03-13 01:00:00,000"}, //
- {"2017-03-12 21:00:00,000", "2017-03-13 02:00:00,000"}, //
- {"2017-03-12 22:00:00,000", "2017-03-13 03:00:00,000"}, //
- {"2017-03-12 23:00:00,000", "2017-03-13 04:00:00,000"}, //
- {"2017-03-13 00:00:00,000", "2017-03-13 05:00:00,000"}, //
- {"2017-03-13 01:00:00,000", "2017-03-13 06:00:00,000"}, //
- {"2017-03-13 02:00:00,000", "2017-03-13 07:00:00,000"}, //
- {"2017-03-13 03:00:00,000", "2017-03-13 08:00:00,000"}, //
- {"2017-03-13 04:00:00,000", "2017-03-13 09:00:00,000"}, //
- {"2017-03-13 05:00:00,000", "2017-03-13 10:00:00,000"}, //
- {"2017-03-13 06:00:00,000", "2017-03-13 11:00:00,000"}, //
- };
-
- final TimeZone tz = TimeZone.getTimeZone("US/Central");
- for (int i = 0; i < 36; i++) {
- final Date date = calendar.getTime();
- assertEquals("SimpleDateFormat TZ=US Central", expectedDstAndNoDst[i][0], usCentral.format(date));
- assertEquals("SimpleDateFormat TZ=UTC", expectedDstAndNoDst[i][1], utc.format(date));
- assertEquals(
- "FixedDateFormat TZ=US Central", expectedDstAndNoDst[i][0], fixedUsCentral.format(date.getTime()));
- assertEquals("FixedDateFormat TZ=UTC", expectedDstAndNoDst[i][1], fixedUtc.format(date.getTime()));
- calendar.add(Calendar.HOUR_OF_DAY, 1);
- }
- }
-
- @Test
- public void testDaylightSavingToWinterTime() throws Exception {
- final Calendar calendar = Calendar.getInstance();
- calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse("2017-11-05 00:00:00 UTC"));
-
- final SimpleDateFormat usCentral = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS", Locale.US);
- usCentral.setTimeZone(TimeZone.getTimeZone("US/Central"));
-
- final SimpleDateFormat utc = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS", Locale.US);
- utc.setTimeZone(TimeZone.getTimeZone("UTC"));
-
- final FixedDateFormat fixedUsCentral = FixedDateFormat.create(DEFAULT, TimeZone.getTimeZone("US/Central"));
- final FixedDateFormat fixedUtc = FixedDateFormat.create(DEFAULT, TimeZone.getTimeZone("UTC"));
-
- final String[][] expectedDstAndNoDst = {
- // US/Central, UTC
- {"2017-11-04 19:00:00,000", "2017-11-05 00:00:00,000"}, //
- {"2017-11-04 20:00:00,000", "2017-11-05 01:00:00,000"}, //
- {"2017-11-04 21:00:00,000", "2017-11-05 02:00:00,000"}, //
- {"2017-11-04 22:00:00,000", "2017-11-05 03:00:00,000"}, //
- {"2017-11-04 23:00:00,000", "2017-11-05 04:00:00,000"}, //
- {"2017-11-05 00:00:00,000", "2017-11-05 05:00:00,000"}, //
- {"2017-11-05 01:00:00,000", "2017-11-05 06:00:00,000"}, // DST jump at 2am US central time
- {"2017-11-05 01:00:00,000", "2017-11-05 07:00:00,000"}, //
- {"2017-11-05 02:00:00,000", "2017-11-05 08:00:00,000"}, //
- {"2017-11-05 03:00:00,000", "2017-11-05 09:00:00,000"}, //
- {"2017-11-05 04:00:00,000", "2017-11-05 10:00:00,000"}, //
- {"2017-11-05 05:00:00,000", "2017-11-05 11:00:00,000"}, //
- {"2017-11-05 06:00:00,000", "2017-11-05 12:00:00,000"}, //
- {"2017-11-05 07:00:00,000", "2017-11-05 13:00:00,000"}, //
- {"2017-11-05 08:00:00,000", "2017-11-05 14:00:00,000"}, //
- {"2017-11-05 09:00:00,000", "2017-11-05 15:00:00,000"}, //
- {"2017-11-05 10:00:00,000", "2017-11-05 16:00:00,000"}, //
- {"2017-11-05 11:00:00,000", "2017-11-05 17:00:00,000"}, //
- {"2017-11-05 12:00:00,000", "2017-11-05 18:00:00,000"}, //
- {"2017-11-05 13:00:00,000", "2017-11-05 19:00:00,000"}, //
- {"2017-11-05 14:00:00,000", "2017-11-05 20:00:00,000"}, //
- {"2017-11-05 15:00:00,000", "2017-11-05 21:00:00,000"}, //
- {"2017-11-05 16:00:00,000", "2017-11-05 22:00:00,000"}, //
- {"2017-11-05 17:00:00,000", "2017-11-05 23:00:00,000"}, // 24
- {"2017-11-05 18:00:00,000", "2017-11-06 00:00:00,000"}, //
- {"2017-11-05 19:00:00,000", "2017-11-06 01:00:00,000"}, //
- {"2017-11-05 20:00:00,000", "2017-11-06 02:00:00,000"}, //
- {"2017-11-05 21:00:00,000", "2017-11-06 03:00:00,000"}, //
- {"2017-11-05 22:00:00,000", "2017-11-06 04:00:00,000"}, //
- {"2017-11-05 23:00:00,000", "2017-11-06 05:00:00,000"}, //
- {"2017-11-06 00:00:00,000", "2017-11-06 06:00:00,000"}, //
- {"2017-11-06 01:00:00,000", "2017-11-06 07:00:00,000"}, //
- {"2017-11-06 02:00:00,000", "2017-11-06 08:00:00,000"}, //
- {"2017-11-06 03:00:00,000", "2017-11-06 09:00:00,000"}, //
- {"2017-11-06 04:00:00,000", "2017-11-06 10:00:00,000"}, //
- {"2017-11-06 05:00:00,000", "2017-11-06 11:00:00,000"}, //
- };
-
- final TimeZone tz = TimeZone.getTimeZone("US/Central");
- for (int i = 0; i < 36; i++) {
- final Date date = calendar.getTime();
- // System.out.println(usCentral.format(date) + ", Fixed: " + fixedUsCentral.format(date.getTime()) + ", utc:
- // " + utc.format(date));
- assertEquals("SimpleDateFormat TZ=US Central", expectedDstAndNoDst[i][0], usCentral.format(date));
- assertEquals("SimpleDateFormat TZ=UTC", expectedDstAndNoDst[i][1], utc.format(date));
- assertEquals(
- "FixedDateFormat TZ=US Central", expectedDstAndNoDst[i][0], fixedUsCentral.format(date.getTime()));
- assertEquals("FixedDateFormat TZ=UTC", expectedDstAndNoDst[i][1], fixedUtc.format(date.getTime()));
- calendar.add(Calendar.HOUR_OF_DAY, 1);
- }
- }
- /**
- * This test case validates date pattern before and after DST
- * Base Date : 12 Mar 2017
- * Daylight Savings started on : 02:00 AM
- */
- @Test
- public void testFormatLong_goingBackInTime_DST() {
- final Calendar instance = Calendar.getInstance(TimeZone.getTimeZone("EST"));
- instance.set(2017, 2, 12, 2, 0);
- final long now = instance.getTimeInMillis();
- final long start = now - TimeUnit.HOURS.toMillis(1);
- final long end = now + TimeUnit.HOURS.toMillis(1);
-
- for (final FixedFormat format : FixedFormat.values()) {
- final String pattern = format.getPattern();
- if (containsNanos(format) || format.getTimeZoneFormat() != null) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
- }
- final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
- final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
- for (long time = end; time > start; time -= 12345) {
- final String actual = customTF.format(time);
- final String expected = simpleDF.format(new Date(time));
- assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
- }
- }
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatterTest.java
new file mode 100644
index 0000000000..3521d4d410
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatterTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.util.internal.instant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class InstantNumberFormatterTest {
+
+ @ParameterizedTest
+ @MethodSource("testCases")
+ void should_produce_expected_output(
+ final InstantFormatter formatter, final Instant instant, final String expectedOutput) {
+ final String actualOutput = formatter.format(instant);
+ assertThat(actualOutput).isEqualTo(expectedOutput);
+ }
+
+ static Stream<Object[]> testCases() {
+ return Stream.concat(
+ testCases(1581082727, 982123456, new Object[][] {
+ {InstantNumberFormatter.EPOCH_SECONDS, "1581082727.982123456"},
+ {InstantNumberFormatter.EPOCH_SECONDS_ROUNDED, "1581082727"},
+ {InstantNumberFormatter.EPOCH_SECONDS_NANOS, "982123456"},
+ {InstantNumberFormatter.EPOCH_MILLIS, "1581082727982.123456"},
+ {InstantNumberFormatter.EPOCH_MILLIS_ROUNDED, "1581082727982"},
+ {InstantNumberFormatter.EPOCH_MILLIS_NANOS, "123456"},
+ {InstantNumberFormatter.EPOCH_NANOS, "1581082727982123456"}
+ }),
+ testCases(1591177590, 5000001, new Object[][] {
+ {InstantNumberFormatter.EPOCH_SECONDS, "1591177590.005000001"},
+ {InstantNumberFormatter.EPOCH_SECONDS_ROUNDED, "1591177590"},
+ {InstantNumberFormatter.EPOCH_SECONDS_NANOS, "5000001"},
+ {InstantNumberFormatter.EPOCH_MILLIS, "1591177590005.000001"},
+ {InstantNumberFormatter.EPOCH_MILLIS_ROUNDED, "1591177590005"},
+ {InstantNumberFormatter.EPOCH_MILLIS_NANOS, "1"},
+ {InstantNumberFormatter.EPOCH_NANOS, "1591177590005000001"}
+ }));
+ }
+
+ private static Stream<Object[]> testCases(
+ long epochSeconds, int epochSecondsNanos, Object[][] formatterAndOutputPairs) {
+ return Arrays.stream(formatterAndOutputPairs).map(formatterAndOutputPair -> {
+ final InstantFormatter formatter = (InstantFormatter) formatterAndOutputPair[0];
+ final String expectedOutput = (String) formatterAndOutputPair[1];
+ final MutableInstant instant = new MutableInstant();
+ instant.initFromEpochSecond(epochSeconds, epochSecondsNanos);
+ return new Object[] {formatter, instant, expectedOutput};
+ });
+ }
+}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
new file mode 100644
index 0000000000..ddb8c31103
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
@@ -0,0 +1,424 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.util.internal.instant;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.sequencePattern;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+import java.util.TimeZone;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.CompositePatternSequence;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.DynamicPatternSequence;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.PatternSequence;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.StaticPatternSequence;
+import org.apache.logging.log4j.util.Constants;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class InstantPatternDynamicFormatterTest {
+
+ @ParameterizedTest
+ @MethodSource("sequencingTestCases")
+ void sequencing_should_work(
+ final String pattern, final ChronoUnit thresholdPrecision, final List<PatternSequence> expectedSequences) {
+ final List<PatternSequence> actualSequences = sequencePattern(pattern, thresholdPrecision);
+ assertThat(actualSequences).isEqualTo(expectedSequences);
+ }
+
+ static List<Arguments> sequencingTestCases() {
+ final List<Arguments> testCases = new ArrayList<>();
+
+ // `SSSX` should be treated constant for daily updates
+ testCases.add(Arguments.of("SSSX", ChronoUnit.DAYS, singletonList(pCom(pDyn("SSS"), pDyn("X")))));
+
+ // `yyyyMMddHHmmssSSSX` instant cache updated hourly
+ testCases.add(Arguments.of(
+ "yyyyMMddHHmmssSSSX",
+ ChronoUnit.HOURS,
+ asList(
+ pCom(pDyn("yyyy"), pDyn("MM"), pDyn("dd"), pDyn("HH")),
+ pCom(pDyn("mm"), pDyn("ss"), pDyn("SSS")),
+ pDyn("X"))));
+
+ // `yyyyMMddHHmmssSSSX` instant cache updated per minute
+ testCases.add(Arguments.of(
+ "yyyyMMddHHmmssSSSX",
+ ChronoUnit.MINUTES,
+ asList(
+ pCom(pDyn("yyyy"), pDyn("MM"), pDyn("dd"), pDyn("HH"), pDyn("mm")),
+ pCom(pDyn("ss"), pDyn("SSS")),
+ pDyn("X"))));
+
+ // ISO9601 instant cache updated daily
+ final String iso8601InstantPattern = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
+ testCases.add(Arguments.of(
+ iso8601InstantPattern,
+ ChronoUnit.DAYS,
+ asList(
+ pCom(pDyn("yyyy"), pSta("-"), pDyn("MM"), pSta("-"), pDyn("dd"), pSta("T")),
+ pCom(
+ pDyn("HH"),
+ pSta(":"),
+ pDyn("mm"),
+ pSta(":"),
+ pDyn("ss"),
+ pSta("."),
+ pDyn("SSS"),
+ pDyn("X")))));
+
+ // ISO9601 instant cache updated per minute
+ testCases.add(Arguments.of(
+ iso8601InstantPattern,
+ ChronoUnit.MINUTES,
+ asList(
+ pCom(
+ pDyn("yyyy"),
+ pSta("-"),
+ pDyn("MM"),
+ pSta("-"),
+ pDyn("dd"),
+ pSta("T"),
+ pDyn("HH"),
+ pSta(":"),
+ pDyn("mm"),
+ pSta(":")),
+ pCom(pDyn("ss"), pSta("."), pDyn("SSS")),
+ pDyn("X"))));
+
+ // ISO9601 instant cache updated per second
+ testCases.add(Arguments.of(
+ iso8601InstantPattern,
+ ChronoUnit.SECONDS,
+ asList(
+ pCom(
+ pDyn("yyyy"),
+ pSta("-"),
+ pDyn("MM"),
+ pSta("-"),
+ pDyn("dd"),
+ pSta("T"),
+ pDyn("HH"),
+ pSta(":"),
+ pDyn("mm"),
+ pSta(":"),
+ pDyn("ss"),
+ pSta(".")),
+ pDyn("SSS"),
+ pDyn("X"))));
+
+ return testCases;
+ }
+
+ private static CompositePatternSequence pCom(final PatternSequence... sequences) {
+ return new CompositePatternSequence(asList(sequences));
+ }
+
+ private static DynamicPatternSequence pDyn(final String pattern) {
+ return new DynamicPatternSequence(pattern);
+ }
+
+ private static StaticPatternSequence pSta(final String literal) {
+ return new StaticPatternSequence(literal);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "S",
+ "SSSSSSS",
+ "SSSSSSSSS",
+ "n",
+ "nn",
+ "N",
+ "NN",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm:ss,SSSSSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSSSSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SXXX"
+ })
+ void should_recognize_patterns_of_nano_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.NANOS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "SSSS",
+ "SSSSS",
+ "SSSSSS",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm:ss,SSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
+ // Single-quoted text containing nanosecond directives
+ "yyyy-MM-dd'S'HH:mm:ss.SSSSSSXXX",
+ "yyyy-MM-dd'n'HH:mm:ss.SSSSSSXXX",
+ "yyyy-MM-dd'N'HH:mm:ss.SSSSSSXXX",
+ })
+ void should_recognize_patterns_of_micro_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.MICROS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "SS",
+ "SSS",
+ "A",
+ "AA",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm:ss,SS",
+ "yyyy-MM-dd HH:mm:ss,SSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
+ // Single-quoted text containing nanosecond directives
+ "yyyy-MM-dd'S'HH:mm:ss.SSSXXX",
+ "yyyy-MM-dd'n'HH:mm:ss.SSSXXX",
+ "yyyy-MM-dd'N'HH:mm:ss.SSSXXX",
+ })
+ void should_recognize_patterns_of_milli_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.MILLIS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "s",
+ "ss",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm:s",
+ "yyyy-MM-dd HH:mm:ss",
+ "yyyy-MM-dd'T'HH:mm:ss",
+ "HH:mm:s",
+ // Single-quoted text containing nanosecond and millisecond directives
+ "yyyy-MM-dd'S'HH:mm:ss",
+ "yyyy-MM-dd'n'HH:mm:ss",
+ "yyyy-MM-dd'N'HH:mm:ss",
+ "yyyy-MM-dd'A'HH:mm:ss"
+ })
+ void should_recognize_patterns_of_second_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.SECONDS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "m",
+ "mm",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm",
+ "yyyy-MM-dd'T'HH:mm",
+ "HH:mm",
+ // Single-quoted text containing nanosecond and millisecond directives
+ "yyyy-MM-dd'S'HH:mm",
+ "yyyy-MM-dd'n'HH:mm"
+ })
+ void should_recognize_patterns_of_minute_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.MINUTES);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hourPrecisionPatterns")
+ void should_recognize_patterns_of_hour_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.HOURS);
+ }
+
+ static List<String> hourPrecisionPatterns() {
+ final List<String> java8Patterns = new ArrayList<>(asList(
+ // Basics
+ "H",
+ "HH",
+ "a",
+ "h",
+ "K",
+ "k",
+ "H",
+ "Z",
+ "x",
+ "X",
+ "O",
+ "z",
+ "VV",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH",
+ "yyyy-MM-dd'T'HH",
+ "yyyy-MM-dd HH x",
+ "yyyy-MM-dd'T'HH XX",
+ "ddHH",
+ // Single-quoted text containing nanosecond and millisecond directives
+ "yyyy-MM-dd'S'HH",
+ "yyyy-MM-dd'n'HH"));
+ if (Constants.JAVA_MAJOR_VERSION > 8) {
+ java8Patterns.add("B");
+ java8Patterns.add("v");
+ }
+ return java8Patterns;
+ }
+
+ private static void assertPatternPrecision(final String pattern, final ChronoUnit expectedPrecision) {
+ final InstantPatternFormatter formatter =
+ new InstantPatternDynamicFormatter(pattern, Locale.getDefault(), TimeZone.getDefault());
+ assertThat(formatter.getPrecision()).as("pattern=`%s`", pattern).isEqualTo(expectedPrecision);
+ }
+
+ @ParameterizedTest
+ @MethodSource("formatterInputs")
+ void output_should_match_DateTimeFormatter(
+ final String pattern, final Locale locale, final TimeZone timeZone, final MutableInstant instant) {
+ final String log4jOutput = formatInstant(pattern, locale, timeZone, instant);
+ final String javaOutput = DateTimeFormatter.ofPattern(pattern, locale)
+ .withZone(timeZone.toZoneId())
+ .format(instant);
+ assertThat(log4jOutput).isEqualTo(javaOutput);
+ }
+
+ static Stream<Arguments> formatterInputs() {
+ return Stream.of(
+ // Complete list of `FixedDateFormat`-supported patterns in version `2.24.1`
+ "HH:mm:ss,SSS",
+ "HH:mm:ss,SSSSSS",
+ "HH:mm:ss,SSSSSSSSS",
+ "HH:mm:ss.SSS",
+ "yyyyMMddHHmmssSSS",
+ "dd MMM yyyy HH:mm:ss,SSS",
+ "dd MMM yyyy HH:mm:ss.SSS",
+ "yyyy-MM-dd HH:mm:ss,SSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSSSSS",
+ "yyyy-MM-dd HH:mm:ss.SSS",
+ "yyyyMMdd'T'HHmmss,SSS",
+ "yyyyMMdd'T'HHmmss.SSS",
+ "yyyy-MM-dd'T'HH:mm:ss,SSS",
+ "yyyy-MM-dd'T'HH:mm:ss,SSSx",
+ "yyyy-MM-dd'T'HH:mm:ss,SSSxx",
+ "yyyy-MM-dd'T'HH:mm:ss,SSSxxx",
+ "yyyy-MM-dd'T'HH:mm:ss.SSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
+ "dd/MM/yy HH:mm:ss.SSS",
+ "dd/MM/yyyy HH:mm:ss.SSS")
+ .flatMap(InstantPatternDynamicFormatterTest::formatterInputs);
+ }
+
+ private static final Random RANDOM = new Random(0);
+
+ private static final Locale[] LOCALES = Locale.getAvailableLocales();
+
+ private static final TimeZone[] TIME_ZONES =
+ Arrays.stream(TimeZone.getAvailableIDs()).map(TimeZone::getTimeZone).toArray(TimeZone[]::new);
+
+ static Stream<Arguments> formatterInputs(final String pattern) {
+ return IntStream.range(0, 500).mapToObj(ignoredIndex -> {
+ final Locale locale = LOCALES[RANDOM.nextInt(LOCALES.length)];
+ final TimeZone timeZone = TIME_ZONES[RANDOM.nextInt(TIME_ZONES.length)];
+ final MutableInstant instant = randomInstant();
+ return Arguments.of(pattern, locale, timeZone, instant);
+ });
+ }
+
+ private static MutableInstant randomInstant() {
+ final MutableInstant instant = new MutableInstant();
+ final long epochSecond = RANDOM.nextInt(1_621_280_470); // 2021-05-17 21:41:10
+ final int epochSecondNano = randomNanos();
+ instant.initFromEpochSecond(epochSecond, epochSecondNano);
+ return instant;
+ }
+
+ private static int randomNanos() {
+ int total = 0;
+ for (int digitIndex = 0; digitIndex < 9; digitIndex++) {
+ int number;
+ do {
+ number = RANDOM.nextInt(10);
+ } while (digitIndex == 0 && number == 0);
+ total = total * 10 + number;
+ }
+ return total;
+ }
+
+ private static String formatInstant(
+ final String pattern, final Locale locale, final TimeZone timeZone, final MutableInstant instant) {
+ final InstantPatternFormatter formatter = new InstantPatternDynamicFormatter(pattern, locale, timeZone);
+ final StringBuilder buffer = new StringBuilder();
+ formatter.formatTo(buffer, instant);
+ return buffer.toString();
+ }
+
+ @ParameterizedTest
+ @MethodSource("formatterInputs")
+ void verify_manually_computed_sub_minute_precision_values(
+ final String ignoredPattern,
+ final Locale ignoredLocale,
+ final TimeZone timeZone,
+ final MutableInstant instant) {
+ final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
+ "HH:mm:ss.S-SS-SSS-SSSS-SSSSS-SSSSSS-SSSSSSS-SSSSSSSS-SSSSSSSSS|n")
+ .withZone(timeZone.toZoneId());
+ final String formatterOutput = formatter.format(instant);
+ final int offsetMillis = timeZone.getOffset(instant.getEpochMillisecond());
+ final long adjustedEpochSeconds = (instant.getEpochMillisecond() + offsetMillis) / 1000;
+ // 86400 seconds per day, 3600 seconds per hour
+ final int local_H = (int) ((adjustedEpochSeconds % 86400L) / 3600L);
+ final int local_m = (int) ((adjustedEpochSeconds / 60) % 60);
+ final int local_s = (int) (adjustedEpochSeconds % 60);
+ final int local_S = instant.getNanoOfSecond() / 100000000;
+ final int local_SS = instant.getNanoOfSecond() / 10000000;
+ final int local_SSS = instant.getNanoOfSecond() / 1000000;
+ final int local_SSSS = instant.getNanoOfSecond() / 100000;
+ final int local_SSSSS = instant.getNanoOfSecond() / 10000;
+ final int local_SSSSSS = instant.getNanoOfSecond() / 1000;
+ final int local_SSSSSSS = instant.getNanoOfSecond() / 100;
+ final int local_SSSSSSSS = instant.getNanoOfSecond() / 10;
+ final int local_SSSSSSSSS = instant.getNanoOfSecond();
+ final int local_n = instant.getNanoOfSecond();
+ final String output = String.format(
+ "%02d:%02d:%02d.%d-%d-%d-%d-%d-%d-%d-%d-%d|%d",
+ local_H,
+ local_m,
+ local_s,
+ local_S,
+ local_SS,
+ local_SSS,
+ local_SSSS,
+ local_SSSSS,
+ local_SSSSSS,
+ local_SSSSSSS,
+ local_SSSSSSSS,
+ local_SSSSSSSSS,
+ local_n);
+ assertThat(output).isEqualTo(formatterOutput);
+ }
+}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
new file mode 100644
index 0000000000..b25fb85d74
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
@@ -0,0 +1,292 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.util.internal.instant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.time.temporal.ChronoUnit;
+import java.util.Locale;
+import java.util.Random;
+import java.util.TimeZone;
+import java.util.function.Function;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class InstantPatternThreadLocalCachedFormatterTest {
+
+ private static final Locale LOCALE = Locale.getDefault();
+
+ private static final TimeZone TIME_ZONE = TimeZone.getDefault();
+
+ @ParameterizedTest
+ @MethodSource("getterTestCases")
+ void getters_should_work(
+ final Function<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter> cachedFormatterSupplier,
+ final String pattern,
+ final Locale locale,
+ final TimeZone timeZone) {
+ final InstantPatternDynamicFormatter dynamicFormatter =
+ new InstantPatternDynamicFormatter(pattern, locale, timeZone);
+ final InstantPatternThreadLocalCachedFormatter cachedFormatter =
+ cachedFormatterSupplier.apply(dynamicFormatter);
+ assertThat(cachedFormatter.getPattern()).isEqualTo(pattern);
+ assertThat(cachedFormatter.getLocale()).isEqualTo(locale);
+ assertThat(cachedFormatter.getTimeZone()).isEqualTo(timeZone);
+ }
+
+ static Object[][] getterTestCases() {
+
+ // Choosing two different locale & time zone pairs to ensure having one that doesn't match the system default
+ final Locale locale1 = Locale.forLanguageTag("nl_NL");
+ final Locale locale2 = Locale.forLanguageTag("tr_TR");
+ final String[] timeZoneIds = TimeZone.getAvailableIDs();
+ final int timeZone1IdIndex = new Random(0).nextInt(timeZoneIds.length);
+ final int timeZone2IdIndex = (timeZone1IdIndex + 1) % timeZoneIds.length;
+ final TimeZone timeZone1 = TimeZone.getTimeZone(timeZoneIds[timeZone1IdIndex]);
+ final TimeZone timeZone2 = TimeZone.getTimeZone(timeZoneIds[timeZone2IdIndex]);
+
+ // Create test cases
+ return new Object[][] {
+ // For `ofMilliPrecision()`
+ {
+ (Function<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter>)
+ InstantPatternThreadLocalCachedFormatter::ofMilliPrecision,
+ "HH:mm.SSS",
+ locale1,
+ timeZone1
+ },
+ {
+ (Function<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter>)
+ InstantPatternThreadLocalCachedFormatter::ofMilliPrecision,
+ "HH:mm.SSS",
+ locale2,
+ timeZone2
+ },
+ // For `ofSecondPrecision()`
+ {
+ (Function<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter>)
+ InstantPatternThreadLocalCachedFormatter::ofSecondPrecision,
+ "yyyy",
+ locale1,
+ timeZone1
+ },
+ {
+ (Function<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter>)
+ InstantPatternThreadLocalCachedFormatter::ofSecondPrecision,
+ "yyyy",
+ locale2,
+ timeZone2
+ }
+ };
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"S", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS", "n", "N"})
+ void ofMilliPrecision_should_fail_on_inconsistent_precision(final String subMilliPattern) {
+ final InstantPatternDynamicFormatter dynamicFormatter =
+ new InstantPatternDynamicFormatter(subMilliPattern, LOCALE, TIME_ZONE);
+ assertThatThrownBy(() -> InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(dynamicFormatter))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(
+ "instant formatter `%s` is of `%s` precision, whereas the requested cache precision is `%s`",
+ dynamicFormatter, dynamicFormatter.getPrecision(), ChronoUnit.MILLIS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"SSS", "s", "ss", "m", "mm", "H", "HH"})
+ void ofMilliPrecision_should_truncate_precision_to_milli(final String superMilliPattern) {
+ final InstantPatternDynamicFormatter dynamicFormatter =
+ new InstantPatternDynamicFormatter(superMilliPattern, LOCALE, TIME_ZONE);
+ final InstantPatternThreadLocalCachedFormatter cachedFormatter =
+ InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(dynamicFormatter);
+ assertThat(cachedFormatter.getPrecision()).isEqualTo(ChronoUnit.MILLIS);
+ assertThat(cachedFormatter.getPrecision().compareTo(dynamicFormatter.getPrecision()))
+ .isLessThanOrEqualTo(0);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {"S", "SS", "SSS", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS", "n", "N", "A"})
+ void ofSecondPrecision_should_fail_on_inconsistent_precision(final String subSecondPattern) {
+ final InstantPatternDynamicFormatter dynamicFormatter =
+ new InstantPatternDynamicFormatter(subSecondPattern, LOCALE, TIME_ZONE);
+ assertThatThrownBy(() -> InstantPatternThreadLocalCachedFormatter.ofSecondPrecision(dynamicFormatter))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(
+ "instant formatter `%s` is of `%s` precision, whereas the requested cache precision is `%s`",
+ dynamicFormatter, dynamicFormatter.getPrecision(), ChronoUnit.SECONDS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"s", "ss", "m", "mm", "H", "HH"})
+ void ofSecondPrecision_should_truncate_precision_to_second(final String superSecondPattern) {
+ final InstantPatternDynamicFormatter dynamicFormatter =
+ new InstantPatternDynamicFormatter(superSecondPattern, LOCALE, TIME_ZONE);
+ final InstantPatternThreadLocalCachedFormatter cachedFormatter =
+ InstantPatternThreadLocalCachedFormatter.ofSecondPrecision(dynamicFormatter);
+ assertThat(cachedFormatter.getPrecision()).isEqualTo(ChronoUnit.SECONDS);
+ assertThat(cachedFormatter.getPrecision().compareTo(dynamicFormatter.getPrecision()))
+ .isLessThanOrEqualTo(0);
+ }
+
+ private static final MutableInstant INSTANT0 = createInstant(0, 0);
+
+ @Test
+ void ofMilliPrecision_should_cache() {
+
+ // Mock a pattern formatter
+ final InstantPatternFormatter patternFormatter = mock(InstantPatternFormatter.class);
+ when(patternFormatter.getPrecision()).thenReturn(ChronoUnit.MILLIS);
+
+ // Configure the pattern formatter for the 1st instant
+ final Instant instant1 = INSTANT0;
+ final String output1 = "instant1";
+ doAnswer(invocation -> {
+ final StringBuilder buffer = invocation.getArgument(0);
+ buffer.append(output1);
+ return null;
+ })
+ .when(patternFormatter)
+ .formatTo(any(StringBuilder.class), eq(instant1));
+
+ // Create a 2nd distinct instant that shares the same milliseconds with the 1st instant.
+ // That is, the 2nd instant should trigger a cache hit.
+ final MutableInstant instant2 = offsetInstant(instant1, 0, 1);
+ assertThat(instant1.getEpochMillisecond()).isEqualTo(instant2.getEpochMillisecond());
+ assertThat(instant1).isNotEqualTo(instant2);
+
+ // Configure the pattern for a 3rd distinct instant.
+ // The 3rd instant should be of different milliseconds with the 1st (and 2nd) instants to trigger a cache miss.
+ final MutableInstant instant3 = offsetInstant(instant2, 1, 0);
+ assertThat(instant2.getEpochMillisecond()).isNotEqualTo(instant3.getEpochMillisecond());
+ final String output3 = "instant3";
+ doAnswer(invocation -> {
+ final StringBuilder buffer = invocation.getArgument(0);
+ buffer.append(output3);
+ return null;
+ })
+ .when(patternFormatter)
+ .formatTo(any(StringBuilder.class), eq(instant3));
+
+ // Create a 4th distinct instant that shares the same milliseconds with the 3rd instant.
+ // That is, the 4th instant should trigger a cache hit.
+ final MutableInstant instant4 = offsetInstant(instant3, 0, 1);
+ assertThat(instant3.getEpochMillisecond()).isEqualTo(instant4.getEpochMillisecond());
+ assertThat(instant3).isNotEqualTo(instant4);
+
+ // Create the cached formatter and verify its output
+ final InstantFormatter cachedFormatter =
+ InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(patternFormatter);
+ assertThat(cachedFormatter.format(instant1)).isEqualTo(output1); // Cache miss
+ assertThat(cachedFormatter.format(instant2)).isEqualTo(output1); // Cache hit
+ assertThat(cachedFormatter.format(instant2)).isEqualTo(output1); // Repeated cache hit
+ assertThat(cachedFormatter.format(instant3)).isEqualTo(output3); // Cache miss
+ assertThat(cachedFormatter.format(instant4)).isEqualTo(output3); // Cache hit
+ assertThat(cachedFormatter.format(instant4)).isEqualTo(output3); // Repeated cache hit
+
+ // Verify the pattern formatter interaction
+ verify(patternFormatter).getPrecision();
+ verify(patternFormatter).formatTo(any(StringBuilder.class), eq(instant1));
+ verify(patternFormatter).formatTo(any(StringBuilder.class), eq(instant3));
+ verifyNoMoreInteractions(patternFormatter);
+ }
+
+ @Test
+ void ofSecondPrecision_should_cache() {
+
+ // Mock a pattern formatter
+ final InstantPatternFormatter patternFormatter = mock(InstantPatternFormatter.class);
+ when(patternFormatter.getPrecision()).thenReturn(ChronoUnit.SECONDS);
+
+ // Configure the pattern formatter for the 1st instant
+ final Instant instant1 = INSTANT0;
+ final String output1 = "instant1";
+ doAnswer(invocation -> {
+ final StringBuilder buffer = invocation.getArgument(0);
+ buffer.append(output1);
+ return null;
+ })
+ .when(patternFormatter)
+ .formatTo(any(StringBuilder.class), eq(instant1));
+
+ // Create a 2nd distinct instant that shares the same seconds with the 1st instant.
+ // That is, the 2nd instant should trigger a cache hit.
+ final MutableInstant instant2 = offsetInstant(instant1, 1, 0);
+ assertThat(instant1.getEpochSecond()).isEqualTo(instant2.getEpochSecond());
+ assertThat(instant1).isNotEqualTo(instant2);
+
+ // Configure the pattern for a 3rd distinct instant.
+ // The 3rd instant should be of different seconds with the 1st (and 2nd) instants to trigger a cache miss.
+ final MutableInstant instant3 = offsetInstant(instant2, 1_000, 0);
+ assertThat(instant2.getEpochSecond()).isNotEqualTo(instant3.getEpochSecond());
+ final String output3 = "instant3";
+ doAnswer(invocation -> {
+ final StringBuilder buffer = invocation.getArgument(0);
+ buffer.append(output3);
+ return null;
+ })
+ .when(patternFormatter)
+ .formatTo(any(StringBuilder.class), eq(instant3));
+
+ // Create a 4th distinct instant that shares the same seconds with the 3rd instant.
+ // That is, the 4th instant should trigger a cache hit.
+ final MutableInstant instant4 = offsetInstant(instant3, 1, 0);
+ assertThat(instant3.getEpochSecond()).isEqualTo(instant4.getEpochSecond());
+ assertThat(instant3).isNotEqualTo(instant4);
+
+ // Create the cached formatter and verify its output
+ final InstantFormatter cachedFormatter =
+ InstantPatternThreadLocalCachedFormatter.ofSecondPrecision(patternFormatter);
+ assertThat(cachedFormatter.format(instant1)).isEqualTo(output1); // Cache miss
+ assertThat(cachedFormatter.format(instant2)).isEqualTo(output1); // Cache hit
+ assertThat(cachedFormatter.format(instant2)).isEqualTo(output1); // Repeated cache hit
+ assertThat(cachedFormatter.format(instant3)).isEqualTo(output3); // Cache miss
+ assertThat(cachedFormatter.format(instant4)).isEqualTo(output3); // Cache hit
+ assertThat(cachedFormatter.format(instant4)).isEqualTo(output3); // Repeated cache hit
+
+ // Verify the pattern formatter interaction
+ verify(patternFormatter).getPrecision();
+ verify(patternFormatter).formatTo(any(StringBuilder.class), eq(instant1));
+ verify(patternFormatter).formatTo(any(StringBuilder.class), eq(instant3));
+ verifyNoMoreInteractions(patternFormatter);
+ }
+
+ private static MutableInstant offsetInstant(
+ final Instant instant, final long epochMillisOffset, final int epochMillisNanosOffset) {
+ final long epochMillis = Math.addExact(instant.getEpochMillisecond(), epochMillisOffset);
+ final int epochMillisNanos = Math.addExact(instant.getNanoOfMillisecond(), epochMillisNanosOffset);
+ return createInstant(epochMillis, epochMillisNanos);
+ }
+
+ private static MutableInstant createInstant(final long epochMillis, final int epochMillisNanos) {
+ final MutableInstant instant = new MutableInstant();
+ instant.initFromEpochMilli(epochMillis, epochMillisNanos);
+ return instant;
+ }
+}
diff --git a/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml b/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml
index d109ef9581..64898bb1dd 100644
--- a/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml
+++ b/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml
@@ -23,7 +23,7 @@
<Appenders>
<RollingFile name="RollingFile" fileName="${filename}"
- filePattern="target/rolling-size-test/rollingtest-%d{yyyy-MM-DD'T'HH-mm-ss-SSS}.log">
+ filePattern="target/rolling-size-test/rollingtest-%d{yyyy-MM-dd'T'HH-mm-ss-SSS}.log">
<PatternLayout>
<Pattern>%m%n</Pattern>
</PatternLayout>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/HtmlLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/HtmlLayout.java
index 6459e57cd9..9212a9bb8e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/HtmlLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/HtmlLayout.java
@@ -131,7 +131,7 @@ public final class HtmlLayout extends AbstractStringLayout {
this.headerSize = headerSize;
this.datePatternConverter = DEFAULT_DATE_PATTERN.equals(datePattern)
? null
- : DatePatternConverter.newInstance(configuration, new String[] {datePattern, timezone});
+ : DatePatternConverter.newInstance(new String[] {datePattern, timezone});
}
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
index 09cc6c7ddd..d4a0bda04b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
@@ -16,22 +16,24 @@
*/
package org.apache.logging.log4j.core.pattern;
+import static java.util.Objects.requireNonNull;
+
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
-import java.util.Objects;
import java.util.TimeZone;
+import java.util.stream.Collectors;
import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
-import org.apache.logging.log4j.kit.recycler.Recycler;
+import org.apache.logging.log4j.core.util.internal.instant.InstantFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantNumberFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
import org.apache.logging.log4j.plugins.Namespace;
import org.apache.logging.log4j.plugins.Plugin;
import org.apache.logging.log4j.util.PerformanceSensitive;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
* Converts and formats the event's date in a StringBuilder.
@@ -40,147 +42,91 @@ import org.apache.logging.log4j.util.PerformanceSensitive;
@Plugin("DatePatternConverter")
@ConverterKeys({"d", "date"})
@PerformanceSensitive("allocation")
+@NullMarked
public final class DatePatternConverter extends LogEventPatternConverter implements ArrayPatternConverter {
- private abstract static class Formatter {
- long previousTime; // for ThreadLocal caching mode
- int nanos;
+ private static final String CLASS_NAME = DatePatternConverter.class.getSimpleName();
- abstract void formatToBuffer(final Instant instant, StringBuilder destination);
+ private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
- public String toPattern() {
- return null;
- }
+ private final InstantFormatter formatter;
- public TimeZone getTimeZone() {
- return TimeZone.getDefault();
- }
+ private DatePatternConverter(@Nullable final String[] options) {
+ super("Date", "date");
+ this.formatter = createFormatter(options);
}
- private static final class PatternFormatter extends Formatter {
- private final FastDateFormat fastDateFormat;
-
- // this field is only used in ThreadLocal caching mode
- private final StringBuilder cachedBuffer = new StringBuilder(64);
-
- PatternFormatter(final FastDateFormat fastDateFormat) {
- this.fastDateFormat = fastDateFormat;
- }
-
- @Override
- void formatToBuffer(final Instant instant, final StringBuilder destination) {
- final long timeMillis = instant.getEpochMillisecond();
- if (previousTime != timeMillis) {
- cachedBuffer.setLength(0);
- fastDateFormat.format(timeMillis, cachedBuffer);
- }
- destination.append(cachedBuffer);
- }
-
- @Override
- public String toPattern() {
- return fastDateFormat.getPattern();
- }
-
- @Override
- public TimeZone getTimeZone() {
- return fastDateFormat.getTimeZone();
+ private static InstantFormatter createFormatter(@Nullable final String[] options) {
+ try {
+ return createFormatterUnsafely(options);
+ } catch (final Exception error) {
+ logOptionReadFailure(options, error, "failed for options: {}, falling back to the default instance");
}
+ return InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build();
}
- private static final class FixedFormatter extends Formatter {
- private final FixedDateFormat fixedDateFormat;
+ private static InstantFormatter createFormatterUnsafely(@Nullable final String[] options) {
- // below fields are only used in ThreadLocal caching mode
- private final char[] cachedBuffer = new char[70]; // max length of formatted date-time in any format < 70
- private int length = 0;
+ // Read options
+ final String pattern = readPattern(options);
+ final TimeZone timeZone = readTimeZone(options);
+ final Locale locale = readLocale(options);
- FixedFormatter(final FixedDateFormat fixedDateFormat) {
- this.fixedDateFormat = fixedDateFormat;
+ // Is it epoch seconds?
+ if ("UNIX".equals(pattern)) {
+ return InstantNumberFormatter.EPOCH_SECONDS_ROUNDED;
}
- @Override
- void formatToBuffer(final Instant instant, final StringBuilder destination) {
- final long epochSecond = instant.getEpochSecond();
- final int nanoOfSecond = instant.getNanoOfSecond();
- if (!fixedDateFormat.isEquivalent(previousTime, nanos, epochSecond, nanoOfSecond)) {
- length = fixedDateFormat.formatInstant(instant, cachedBuffer, 0);
- previousTime = epochSecond;
- nanos = nanoOfSecond;
- }
- destination.append(cachedBuffer, 0, length);
+ // Is it epoch milliseconds?
+ if ("UNIX_MILLIS".equals(pattern)) {
+ return InstantNumberFormatter.EPOCH_MILLIS_ROUNDED;
}
- @Override
- public String toPattern() {
- return fixedDateFormat.getFormat();
- }
-
- @Override
- public TimeZone getTimeZone() {
- return fixedDateFormat.getTimeZone();
- }
+ return InstantPatternFormatter.newBuilder()
+ .setPattern(pattern)
+ .setTimeZone(timeZone)
+ .setLocale(locale)
+ .build();
}
- private static final class UnixFormatter extends Formatter {
-
- @Override
- void formatToBuffer(final Instant instant, final StringBuilder destination) {
- destination.append(instant.getEpochSecond()); // no need for caching
- }
+ private static String readPattern(@Nullable final String[] options) {
+ return options != null && options.length > 0 && options[0] != null ? options[0] : DEFAULT_PATTERN;
}
- private static final class UnixMillisFormatter extends Formatter {
-
- @Override
- void formatToBuffer(final Instant instant, final StringBuilder destination) {
- destination.append(instant.getEpochMillisecond()); // no need for caching
+ private static TimeZone readTimeZone(@Nullable final String[] options) {
+ try {
+ if (options != null && options.length > 1 && options[1] != null) {
+ return TimeZone.getTimeZone(options[1]);
+ }
+ } catch (final Exception error) {
+ logOptionReadFailure(
+ options,
+ error,
+ "failed to read the time zone at index 1 of options: {}, falling back to the default time zone");
}
+ return TimeZone.getDefault();
}
- /**
- * UNIX formatter in seconds (standard).
- */
- private static final String UNIX_FORMAT = "UNIX";
-
- /**
- * UNIX formatter in milliseconds
- */
- private static final String UNIX_MILLIS_FORMAT = "UNIX_MILLIS";
-
- private final String pattern;
-
- private final TimeZone timeZone;
-
- private final Recycler<MutableInstant> mutableInstantRecycler;
-
- private final Recycler<Formatter> formatterRecycler;
-
- /**
- * Private constructor.
- *
- * @param options options, may be null.
- */
- private DatePatternConverter(final Configuration configuration, final String[] options) {
- super("Date", "date");
- final String[] safeOptions = options == null ? null : Arrays.copyOf(options, options.length);
- this.mutableInstantRecycler = configuration.getRecyclerFactory().create(MutableInstant::new);
- this.formatterRecycler = configuration.getRecyclerFactory().create(() -> createFormatter(safeOptions));
- final Formatter formatter = formatterRecycler.acquire();
+ private static Locale readLocale(@Nullable final String[] options) {
try {
- this.pattern = formatter.toPattern();
- this.timeZone = formatter.getTimeZone();
- } finally {
- formatterRecycler.release(formatter);
+ if (options != null && options.length > 2 && options[2] != null) {
+ return Locale.forLanguageTag(options[2]);
+ }
+ } catch (final Exception error) {
+ logOptionReadFailure(
+ options,
+ error,
+ "failed to read the locale at index 2 of options: {}, falling back to the default locale");
}
+ return Locale.getDefault();
}
- private Formatter createFormatter(final String[] options) {
- final FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(options);
- if (fixedDateFormat != null) {
- return createFixedFormatter(fixedDateFormat);
+ private static void logOptionReadFailure(final String[] options, final Exception error, final String message) {
+ if (LOGGER.isWarnEnabled()) {
+ final String quotedOptions =
+ Arrays.stream(options).map(option -> '`' + option + '`').collect(Collectors.joining(", "));
+ LOGGER.warn("[{}] " + message, CLASS_NAME, quotedOptions, error);
}
- return createNonFixedFormatter(options);
}
/**
@@ -189,127 +135,70 @@ public final class DatePatternConverter extends LogEventPatternConverter impleme
* @param options options, may be null.
* @return instance of pattern converter.
*/
- public static DatePatternConverter newInstance(final Configuration configuration, final String[] options) {
- return new DatePatternConverter(configuration, options);
+ public static DatePatternConverter newInstance(final String[] options) {
+ return new DatePatternConverter(options);
}
- private static Formatter createFixedFormatter(final FixedDateFormat fixedDateFormat) {
- return new FixedFormatter(fixedDateFormat);
- }
-
- private static Formatter createNonFixedFormatter(final String[] options) {
- // if we get here, options is a non-null array with at least one element (first of which non-null)
- Objects.requireNonNull(options);
- if (options.length == 0) {
- throw new IllegalArgumentException("Options array must have at least one element");
- }
- Objects.requireNonNull(options[0]);
- final String patternOption = options[0];
- if (UNIX_FORMAT.equals(patternOption)) {
- return new UnixFormatter();
- }
- if (UNIX_MILLIS_FORMAT.equals(patternOption)) {
- return new UnixMillisFormatter();
- }
- // LOG4J2-1149: patternOption may be a name (if a time zone was specified)
- final FixedDateFormat.FixedFormat fixedFormat = FixedDateFormat.FixedFormat.lookup(patternOption);
- final String pattern = fixedFormat == null ? patternOption : fixedFormat.getPattern();
-
- // if the option list contains a TZ option, then set it.
- TimeZone tz = null;
- if (options.length > 1 && options[1] != null) {
- tz = TimeZone.getTimeZone(options[1]);
- }
-
- Locale locale = null;
- if (options.length > 2 && options[2] != null) {
- locale = Locale.forLanguageTag(options[2]);
- }
-
- try {
- final FastDateFormat tempFormat = FastDateFormat.getInstance(pattern, tz, locale);
- return new PatternFormatter(tempFormat);
- } catch (final IllegalArgumentException e) {
- LOGGER.warn("Could not instantiate FastDateFormat with pattern " + pattern, e);
-
- // default to the DEFAULT format
- return createFixedFormatter(FixedDateFormat.create(FixedFormat.DEFAULT, tz));
- }
- }
-
- /**
- * Appends formatted date to string buffer.
- *
- * @param date date
- * @param toAppendTo buffer to which formatted date is appended.
- */
- void format(final Date date, final StringBuilder toAppendTo) {
- format(date.getTime(), toAppendTo);
- }
-
- /**
- * {@inheritDoc}
- */
@Override
- public void format(final LogEvent event, final StringBuilder output) {
- format(event.getInstant(), output);
- }
-
- private void format(final long epochMilli, final StringBuilder output) {
- final MutableInstant instant = mutableInstantRecycler.acquire();
- try {
- instant.initFromEpochMilli(epochMilli, 0);
- format(instant, output);
- } finally {
- mutableInstantRecycler.release(instant);
- }
+ public void format(final LogEvent event, final StringBuilder buffer) {
+ formatter.formatTo(buffer, event.getInstant());
}
- void format(final Instant instant, final StringBuilder output) {
- final Formatter formatter = formatterRecycler.acquire();
- try {
- formatter.formatToBuffer(instant, output);
- } finally {
- formatterRecycler.release(formatter);
- }
- }
-
- /**
- * {@inheritDoc}
- */
@Override
- public void format(final Object obj, final StringBuilder output) {
- if (obj instanceof Date) {
- format((Date) obj, output);
- }
- super.format(obj, output);
+ public void format(@Nullable final Object object, final StringBuilder buffer) {
+ requireNonNull(buffer, "buffer");
+ if (object == null) {
+ return;
+ }
+ if (object instanceof LogEvent logEvent) {
+ format(logEvent, buffer);
+ } else if (object instanceof Date date) {
+ final MutableInstant instant = new MutableInstant();
+ instant.initFromEpochMilli(date.getTime(), 0);
+ formatter.formatTo(buffer, instant);
+ } else if (object instanceof Instant instant) {
+ formatter.formatTo(buffer, instant);
+ } else if (object instanceof Long epochMillis) {
+ final MutableInstant instant = new MutableInstant();
+ instant.initFromEpochMilli(epochMillis, 0);
+ formatter.formatTo(buffer, instant);
+ }
+ LOGGER.warn(
+ "[{}]: unsupported object type `{}`",
+ CLASS_NAME,
+ object.getClass().getCanonicalName());
}
@Override
- public void format(final StringBuilder toAppendTo, final Object... objects) {
- for (final Object obj : objects) {
- if (obj instanceof Date) {
- format(obj, toAppendTo);
- break;
+ public void format(final StringBuilder buffer, @Nullable final Object... objects) {
+ requireNonNull(buffer, "buffer");
+ if (objects != null) {
+ for (final Object object : objects) {
+ if (object instanceof Date date) {
+ format(date, buffer);
+ break;
+ }
}
}
}
/**
- * Gets the pattern string describing this date format.
- *
- * @return the pattern string describing this date format.
+ * @return the pattern string describing this date format or {@code null} if the format does not have a pattern.
*/
+ @Nullable
public String getPattern() {
- return pattern;
+ return (formatter instanceof InstantPatternFormatter)
+ ? ((InstantPatternFormatter) formatter).getPattern()
+ : null;
}
/**
- * Gets the timezone used by this date format.
- *
- * @return the timezone used by this date format.
+ * @return the time zone used by this date format
*/
+ @Nullable
public TimeZone getTimeZone() {
- return timeZone;
+ return (formatter instanceof InstantPatternFormatter)
+ ? ((InstantPatternFormatter) formatter).getTimeZone()
+ : null;
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FileDatePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FileDatePatternConverter.java
index 0e2fbd3efd..4bdda72055 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FileDatePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FileDatePatternConverter.java
@@ -17,7 +17,6 @@
package org.apache.logging.log4j.core.pattern;
import java.util.TimeZone;
-import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.plugins.Namespace;
import org.apache.logging.log4j.plugins.Plugin;
import org.apache.logging.log4j.util.PerformanceSensitive;
@@ -38,8 +37,8 @@ public final class FileDatePatternConverter implements ArrayPatternConverter {
/**
* Private constructor.
*/
- private FileDatePatternConverter(final Configuration configuration, final String... options) {
- delegate = DatePatternConverter.newInstance(configuration, options);
+ private FileDatePatternConverter(final String... options) {
+ delegate = DatePatternConverter.newInstance(options);
}
/**
@@ -48,12 +47,12 @@ public final class FileDatePatternConverter implements ArrayPatternConverter {
* @param options options, may be null.
* @return instance of pattern converter.
*/
- public static FileDatePatternConverter newInstance(final Configuration configuration, final String[] options) {
+ public static FileDatePatternConverter newInstance(final String[] options) {
if (options == null || options.length == 0) {
- return new FileDatePatternConverter(configuration, "yyyy-MM-dd");
+ return new FileDatePatternConverter("yyyy-MM-dd");
}
- return new FileDatePatternConverter(configuration, options);
+ return new FileDatePatternConverter(options);
}
@Override
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/InstantFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/InstantFormatter.java
deleted file mode 100644
index 02544f1d71..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/InstantFormatter.java
+++ /dev/null
@@ -1,317 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time;
-
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.TimeZone;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.status.StatusLogger;
-import org.apache.logging.log4j.util.Strings;
-
-/**
- * A composite {@link Instant} formatter trying to employ either {@link FixedDateFormat}, {@link FastDateFormat}, or {@link DateTimeFormatter} in the given order due to performance reasons.
- * <p>
- * Note that {@code FixedDateFormat} and {@code FastDateFormat} only support millisecond precision.
- * If the pattern asks for a higher precision, {@code DateTimeFormatter} will be employed, which is significantly slower.
- * </p>
- */
-public final class InstantFormatter {
-
- private static final StatusLogger LOGGER = StatusLogger.getLogger();
-
- /**
- * The list of formatter factories in decreasing efficiency order.
- */
- private static final FormatterFactory[] FORMATTER_FACTORIES = {
- new Log4jFixedFormatterFactory(), new Log4jFastFormatterFactory(), new JavaDateTimeFormatterFactory()
- };
-
- private final Formatter formatter;
-
- private InstantFormatter(final Builder builder) {
- this.formatter = Arrays.stream(FORMATTER_FACTORIES)
- .map(formatterFactory -> {
- try {
- return formatterFactory.createIfSupported(
- builder.getPattern(), builder.getLocale(), builder.getTimeZone());
- } catch (final Exception error) {
- LOGGER.warn("skipping the failed formatter factory \"{}\"", formatterFactory, error);
- return null;
- }
- })
- .filter(Objects::nonNull)
- .findFirst()
- .orElseThrow(() -> new AssertionError("could not find a matching formatter"));
- }
-
- public String format(final Instant instant) {
- Objects.requireNonNull(instant, "instant");
- final StringBuilder stringBuilder = new StringBuilder();
- formatter.format(instant, stringBuilder);
- return stringBuilder.toString();
- }
-
- public void format(final Instant instant, final StringBuilder stringBuilder) {
- Objects.requireNonNull(instant, "instant");
- Objects.requireNonNull(stringBuilder, "stringBuilder");
- formatter.format(instant, stringBuilder);
- }
-
- /**
- * Checks if the given {@link Instant}s are equal from the point of view of the employed formatter.
- * <p>
- * This method should be preferred over {@link Object#equals(Object)}.
- * For instance, {@link FixedDateFormat} and {@link FastDateFormat} discard nanoseconds, hence, from their point of view, two different {@code Instant}s are equal if they match up to millisecond precision.
- * </p>
- */
- public boolean isInstantMatching(final Instant instant1, final Instant instant2) {
- return formatter.isInstantMatching(instant1, instant2);
- }
-
- public Class<?> getInternalImplementationClass() {
- return formatter.getInternalImplementationClass();
- }
-
- public static Builder newBuilder() {
- return new Builder();
- }
-
- public static final class Builder {
-
- private String pattern;
-
- private Locale locale = Locale.getDefault();
-
- private TimeZone timeZone = TimeZone.getDefault();
-
- private Builder() {}
-
- public String getPattern() {
- return pattern;
- }
-
- public Builder setPattern(final String pattern) {
- this.pattern = pattern;
- return this;
- }
-
- public Locale getLocale() {
- return locale;
- }
-
- public Builder setLocale(final Locale locale) {
- this.locale = locale;
- return this;
- }
-
- public TimeZone getTimeZone() {
- return timeZone;
- }
-
- public Builder setTimeZone(final TimeZone timeZone) {
- this.timeZone = timeZone;
- return this;
- }
-
- public InstantFormatter build() {
- validate();
- return new InstantFormatter(this);
- }
-
- private void validate() {
- if (Strings.isBlank(pattern)) {
- throw new IllegalArgumentException("blank pattern");
- }
- Objects.requireNonNull(locale, "locale");
- Objects.requireNonNull(timeZone, "timeZone");
- }
- }
-
- private interface FormatterFactory {
-
- Formatter createIfSupported(String pattern, Locale locale, TimeZone timeZone);
- }
-
- private interface Formatter {
-
- Class<?> getInternalImplementationClass();
-
- void format(Instant instant, StringBuilder stringBuilder);
-
- boolean isInstantMatching(Instant instant1, Instant instant2);
- }
-
- private static final class JavaDateTimeFormatterFactory implements FormatterFactory {
-
- @Override
- public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
- return new JavaDateTimeFormatter(pattern, locale, timeZone);
- }
- }
-
- private static final class JavaDateTimeFormatter implements Formatter {
-
- private final DateTimeFormatter formatter;
-
- private final MutableInstant mutableInstant;
-
- private JavaDateTimeFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
- this.formatter =
- DateTimeFormatter.ofPattern(pattern).withLocale(locale).withZone(timeZone.toZoneId());
- this.mutableInstant = new MutableInstant();
- }
-
- @Override
- public Class<?> getInternalImplementationClass() {
- return DateTimeFormatter.class;
- }
-
- @Override
- public void format(final Instant instant, final StringBuilder stringBuilder) {
- if (instant instanceof MutableInstant) {
- formatMutableInstant((MutableInstant) instant, stringBuilder);
- } else {
- formatInstant(instant, stringBuilder);
- }
- }
-
- private void formatMutableInstant(final MutableInstant instant, final StringBuilder stringBuilder) {
- formatter.formatTo(instant, stringBuilder);
- }
-
- private void formatInstant(final Instant instant, final StringBuilder stringBuilder) {
- mutableInstant.initFrom(instant);
- formatMutableInstant(mutableInstant, stringBuilder);
- }
-
- @Override
- public boolean isInstantMatching(final Instant instant1, final Instant instant2) {
- return instant1.getEpochSecond() == instant2.getEpochSecond()
- && instant1.getNanoOfSecond() == instant2.getNanoOfSecond();
- }
- }
-
- private static final class Log4jFastFormatterFactory implements FormatterFactory {
-
- @Override
- public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
- final Log4jFastFormatter formatter = new Log4jFastFormatter(pattern, locale, timeZone);
- final boolean patternSupported = patternSupported(pattern, locale, timeZone, formatter);
- return patternSupported ? formatter : null;
- }
- }
-
- private static final class Log4jFastFormatter implements Formatter {
-
- private final FastDateFormat formatter;
-
- private final Calendar calendar;
-
- private Log4jFastFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
- this.formatter = FastDateFormat.getInstance(pattern, timeZone, locale);
- this.calendar = Calendar.getInstance(timeZone, locale);
- }
-
- @Override
- public Class<?> getInternalImplementationClass() {
- return FastDateFormat.class;
- }
-
- @Override
- public void format(final Instant instant, final StringBuilder stringBuilder) {
- calendar.setTimeInMillis(instant.getEpochMillisecond());
- formatter.format(calendar, stringBuilder);
- }
-
- @Override
- public boolean isInstantMatching(final Instant instant1, final Instant instant2) {
- return instant1.getEpochMillisecond() == instant2.getEpochMillisecond();
- }
- }
-
- private static final class Log4jFixedFormatterFactory implements FormatterFactory {
-
- @Override
- public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
- final FixedDateFormat internalFormatter = FixedDateFormat.createIfSupported(pattern, timeZone.getID());
- if (internalFormatter == null) {
- return null;
- }
- final Log4jFixedFormatter formatter = new Log4jFixedFormatter(internalFormatter);
- final boolean patternSupported = patternSupported(pattern, locale, timeZone, formatter);
- return patternSupported ? formatter : null;
- }
- }
-
- private static final class Log4jFixedFormatter implements Formatter {
-
- private final FixedDateFormat formatter;
-
- private final char[] buffer;
-
- private Log4jFixedFormatter(final FixedDateFormat formatter) {
- this.formatter = formatter;
- this.buffer = new char[formatter.getFormat().length()];
- }
-
- @Override
- public Class<?> getInternalImplementationClass() {
- return FixedDateFormat.class;
- }
-
- @Override
- public void format(final Instant instant, final StringBuilder stringBuilder) {
- final int length = formatter.formatInstant(instant, buffer, 0);
- stringBuilder.append(buffer, 0, length);
- }
-
- @Override
- public boolean isInstantMatching(final Instant instant1, final Instant instant2) {
- return formatter.isEquivalent(
- instant1.getEpochSecond(),
- instant1.getNanoOfSecond(),
- instant2.getEpochSecond(),
- instant2.getNanoOfSecond());
- }
- }
-
- /**
- * Checks if the provided formatter output matches with the one generated by {@link DateTimeFormatter}.
- */
- private static boolean patternSupported(
- final String pattern, final Locale locale, final TimeZone timeZone, final Formatter formatter) {
- final DateTimeFormatter javaFormatter =
- DateTimeFormatter.ofPattern(pattern).withLocale(locale).withZone(timeZone.toZoneId());
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(
- // 2021-05-17 21:41:10
- 1621280470,
- // Using the highest nanosecond precision possible to differentiate formatters only supporting
- // millisecond precision.
- 123_456_789);
- final String expectedFormat = javaFormatter.format(instant);
- final StringBuilder stringBuilder = new StringBuilder();
- formatter.format(instant, stringBuilder);
- final String actualFormat = stringBuilder.toString();
- return expectedFormat.equals(actualFormat);
- }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/DatePrinter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/DatePrinter.java
deleted file mode 100644
index 7cebb78f60..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/DatePrinter.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time.internal.format;
-
-import java.text.FieldPosition;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * DatePrinter is the "missing" interface for the format methods of
- * {@link java.text.DateFormat}. You can obtain an object implementing this
- * interface by using one of the FastDateFormat factory methods.
- * <p>
- * Warning: Since binary compatible methods may be added to this interface in any
- * release, developers are not expected to implement this interface.
- * </p>
- *
- * <p>
- * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
- * </p>
- *
- * @since Apache Commons Lang 3.2
- */
-public interface DatePrinter {
-
- /**
- * <p>Formats a millisecond {@code long} value.</p>
- *
- * @param millis the millisecond value to format
- * @return the formatted string
- * @since 2.1
- */
- String format(long millis);
-
- /**
- * <p>Formats a {@code Date} object using a {@code GregorianCalendar}.</p>
- *
- * @param date the date to format
- * @return the formatted string
- */
- String format(Date date);
-
- /**
- * <p>Formats a {@code Calendar} object.</p>
- * The TimeZone set on the Calendar is only used to adjust the time offset.
- * The TimeZone specified during the construction of the Parser will determine the TimeZone
- * used in the formatted string.
- *
- * @param calendar the calendar to format.
- * @return the formatted string
- */
- String format(Calendar calendar);
-
- /**
- * <p>Formats a millisecond {@code long} value into the
- * supplied {@code Appendable}.</p>
- *
- * @param millis the millisecond value to format
- * @param buf the buffer to format into
- * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
- * @return the specified string buffer
- * @since 3.5
- */
- <B extends Appendable> B format(long millis, B buf);
-
- /**
- * <p>Formats a {@code Date} object into the
- * supplied {@code Appendable} using a {@code GregorianCalendar}.</p>
- *
- * @param date the date to format
- * @param buf the buffer to format into
- * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
- * @return the specified string buffer
- * @since 3.5
- */
- <B extends Appendable> B format(Date date, B buf);
-
- /**
- * <p>Formats a {@code Calendar} object into the supplied {@code Appendable}.</p>
- * The TimeZone set on the Calendar is only used to adjust the time offset.
- * The TimeZone specified during the construction of the Parser will determine the TimeZone
- * used in the formatted string.
- *
- * @param calendar the calendar to format
- * @param buf the buffer to format into
- * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
- * @return the specified string buffer
- * @since 3.5
- */
- <B extends Appendable> B format(Calendar calendar, B buf);
-
- // Accessors
- // -----------------------------------------------------------------------
- /**
- * <p>Gets the pattern used by this printer.</p>
- *
- * @return the pattern, {@link java.text.SimpleDateFormat} compatible
- */
- String getPattern();
-
- /**
- * <p>Gets the time zone used by this printer.</p>
- *
- * <p>This zone is always used for {@code Date} printing. </p>
- *
- * @return the time zone
- */
- TimeZone getTimeZone();
-
- /**
- * <p>Gets the locale used by this printer.</p>
- *
- * @return the locale
- */
- Locale getLocale();
-
- /**
- * <p>Formats a {@code Date}, {@code Calendar} or
- * {@code Long} (milliseconds) object.</p>
- *
- * @param obj the object to format
- * @param toAppendTo the buffer to append to
- * @param pos the position - ignored
- * @return the buffer passed in
- * @see java.text.DateFormat#format(Object, StringBuffer, FieldPosition)
- */
- StringBuilder format(Object obj, StringBuilder toAppendTo, FieldPosition pos);
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDateFormat.java
deleted file mode 100644
index b20046ea06..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDateFormat.java
+++ /dev/null
@@ -1,588 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time.internal.format;
-
-import java.text.DateFormat;
-import java.text.FieldPosition;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * <p>FastDateFormat is a fast and thread-safe version of
- * {@link java.text.SimpleDateFormat}.</p>
- *
- * <p>To obtain an instance of FastDateFormat, use one of the static factory methods:
- * {@link #getInstance(String, TimeZone, Locale)}, {@link #getDateInstance(int, TimeZone, Locale)},
- * {@link #getTimeInstance(int, TimeZone, Locale)}, or {@link #getDateTimeInstance(int, int, TimeZone, Locale)}
- * </p>
- *
- * <p>Since FastDateFormat is thread safe, you can use a static member instance:</p>
- * <code>
- * private static final FastDateFormat DATE_FORMATTER = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.SHORT);
- * </code>
- *
- * <p>This class can be used as a direct replacement to
- * {@code SimpleDateFormat} in most formatting and parsing situations.
- * This class is especially useful in multi-threaded server environments.
- * {@code SimpleDateFormat} is not thread-safe in any JDK version,
- * nor will it be as Sun have closed the bug/RFE.
- * </p>
- *
- * <p>All patterns are compatible with
- * SimpleDateFormat (except time zones and some year patterns - see below).</p>
- *
- * <p>Since 3.2, FastDateFormat supports parsing as well as printing.</p>
- *
- * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
- * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
- * This pattern letter can be used here (on all JDK versions).</p>
- *
- * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
- * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}).
- * This introduces a minor incompatibility with Java 1.4, but at a gain of
- * useful functionality.</p>
- *
- * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
- * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
- * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
- * 'YYY' will be formatted as '2003', while it was '03' in former Java
- * versions. FastDateFormat implements the behavior of Java 7.</p>
- *
- * <p>
- * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
- * </p>
- *
- * @since Apache Commons Lang 2.0
- */
-public class FastDateFormat extends Format implements DatePrinter {
-
- /**
- * FULL locale dependent date or time style.
- */
- public static final int FULL = DateFormat.FULL;
-
- /**
- * LONG locale dependent date or time style.
- */
- public static final int LONG = DateFormat.LONG;
-
- /**
- * MEDIUM locale dependent date or time style.
- */
- public static final int MEDIUM = DateFormat.MEDIUM;
-
- /**
- * SHORT locale dependent date or time style.
- */
- public static final int SHORT = DateFormat.SHORT;
-
- private static final FormatCache<FastDateFormat> cache = new FormatCache<FastDateFormat>() {
- @Override
- protected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
- return new FastDateFormat(pattern, timeZone, locale);
- }
- };
-
- private final FastDatePrinter printer;
-
- // -----------------------------------------------------------------------
- /**
- * <p>Gets a formatter instance using the default pattern in the
- * default locale.</p>
- *
- * @return a date/time formatter
- */
- public static FastDateFormat getInstance() {
- return cache.getInstance();
- }
-
- /**
- * <p>Gets a formatter instance using the specified pattern in the
- * default locale.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible
- * pattern
- * @return a pattern based date/time formatter
- * @throws IllegalArgumentException if pattern is invalid
- */
- public static FastDateFormat getInstance(final String pattern) {
- return cache.getInstance(pattern, null, null);
- }
-
- /**
- * <p>Gets a formatter instance using the specified pattern and
- * time zone.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible
- * pattern
- * @param timeZone optional time zone, overrides time zone of
- * formatted date
- * @return a pattern based date/time formatter
- * @throws IllegalArgumentException if pattern is invalid
- */
- public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone) {
- return cache.getInstance(pattern, timeZone, null);
- }
-
- /**
- * <p>Gets a formatter instance using the specified pattern and
- * locale.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible
- * pattern
- * @param locale optional locale, overrides system locale
- * @return a pattern based date/time formatter
- * @throws IllegalArgumentException if pattern is invalid
- */
- public static FastDateFormat getInstance(final String pattern, final Locale locale) {
- return cache.getInstance(pattern, null, locale);
- }
-
- /**
- * <p>Gets a formatter instance using the specified pattern, time zone
- * and locale.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible
- * pattern
- * @param timeZone optional time zone, overrides time zone of
- * formatted date
- * @param locale optional locale, overrides system locale
- * @return a pattern based date/time formatter
- * @throws IllegalArgumentException if pattern is invalid
- * or {@code null}
- */
- public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
- return cache.getInstance(pattern, timeZone, locale);
- }
-
- // -----------------------------------------------------------------------
- /**
- * <p>Gets a date formatter instance using the specified style in the
- * default time zone and locale.</p>
- *
- * @param style date style: FULL, LONG, MEDIUM, or SHORT
- * @return a localized standard date formatter
- * @throws IllegalArgumentException if the Locale has no date
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getDateInstance(final int style) {
- return cache.getDateInstance(style, null, null);
- }
-
- /**
- * <p>Gets a date formatter instance using the specified style and
- * locale in the default time zone.</p>
- *
- * @param style date style: FULL, LONG, MEDIUM, or SHORT
- * @param locale optional locale, overrides system locale
- * @return a localized standard date formatter
- * @throws IllegalArgumentException if the Locale has no date
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getDateInstance(final int style, final Locale locale) {
- return cache.getDateInstance(style, null, locale);
- }
-
- /**
- * <p>Gets a date formatter instance using the specified style and
- * time zone in the default locale.</p>
- *
- * @param style date style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted date
- * @return a localized standard date formatter
- * @throws IllegalArgumentException if the Locale has no date
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) {
- return cache.getDateInstance(style, timeZone, null);
- }
-
- /**
- * <p>Gets a date formatter instance using the specified style, time
- * zone and locale.</p>
- *
- * @param style date style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted date
- * @param locale optional locale, overrides system locale
- * @return a localized standard date formatter
- * @throws IllegalArgumentException if the Locale has no date
- * pattern defined
- */
- public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone, final Locale locale) {
- return cache.getDateInstance(style, timeZone, locale);
- }
-
- // -----------------------------------------------------------------------
- /**
- * <p>Gets a time formatter instance using the specified style in the
- * default time zone and locale.</p>
- *
- * @param style time style: FULL, LONG, MEDIUM, or SHORT
- * @return a localized standard time formatter
- * @throws IllegalArgumentException if the Locale has no time
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getTimeInstance(final int style) {
- return cache.getTimeInstance(style, null, null);
- }
-
- /**
- * <p>Gets a time formatter instance using the specified style and
- * locale in the default time zone.</p>
- *
- * @param style time style: FULL, LONG, MEDIUM, or SHORT
- * @param locale optional locale, overrides system locale
- * @return a localized standard time formatter
- * @throws IllegalArgumentException if the Locale has no time
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getTimeInstance(final int style, final Locale locale) {
- return cache.getTimeInstance(style, null, locale);
- }
-
- /**
- * <p>Gets a time formatter instance using the specified style and
- * time zone in the default locale.</p>
- *
- * @param style time style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted time
- * @return a localized standard time formatter
- * @throws IllegalArgumentException if the Locale has no time
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone) {
- return cache.getTimeInstance(style, timeZone, null);
- }
-
- /**
- * <p>Gets a time formatter instance using the specified style, time
- * zone and locale.</p>
- *
- * @param style time style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted time
- * @param locale optional locale, overrides system locale
- * @return a localized standard time formatter
- * @throws IllegalArgumentException if the Locale has no time
- * pattern defined
- */
- public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone, final Locale locale) {
- return cache.getTimeInstance(style, timeZone, locale);
- }
-
- // -----------------------------------------------------------------------
- /**
- * <p>Gets a date/time formatter instance using the specified style
- * in the default time zone and locale.</p>
- *
- * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT
- * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT
- * @return a localized standard date/time formatter
- * @throws IllegalArgumentException if the Locale has no date/time
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle) {
- return cache.getDateTimeInstance(dateStyle, timeStyle, null, null);
- }
-
- /**
- * <p>Gets a date/time formatter instance using the specified style and
- * locale in the default time zone.</p>
- *
- * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT
- * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT
- * @param locale optional locale, overrides system locale
- * @return a localized standard date/time formatter
- * @throws IllegalArgumentException if the Locale has no date/time
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final Locale locale) {
- return cache.getDateTimeInstance(dateStyle, timeStyle, null, locale);
- }
-
- /**
- * <p>Gets a date/time formatter instance using the specified style and
- * time zone in the default locale.</p>
- *
- * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT
- * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted date
- * @return a localized standard date/time formatter
- * @throws IllegalArgumentException if the Locale has no date/time
- * pattern defined
- * @since 2.1
- */
- public static FastDateFormat getDateTimeInstance(
- final int dateStyle, final int timeStyle, final TimeZone timeZone) {
- return getDateTimeInstance(dateStyle, timeStyle, timeZone, null);
- }
- /**
- * <p>Gets a date/time formatter instance using the specified style,
- * time zone and locale.</p>
- *
- * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT
- * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted date
- * @param locale optional locale, overrides system locale
- * @return a localized standard date/time formatter
- * @throws IllegalArgumentException if the Locale has no date/time
- * pattern defined
- */
- public static FastDateFormat getDateTimeInstance(
- final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
- return cache.getDateTimeInstance(dateStyle, timeStyle, timeZone, locale);
- }
-
- /**
- * <p>Constructs a new FastDateFormat.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible pattern
- * @param timeZone non-null time zone to use
- * @param locale non-null locale to use
- * @param centuryStart The start of the 100 year period to use as the "default century" for 2 digit year parsing. If centuryStart is null, defaults to now - 80 years
- * @throws NullPointerException if pattern, timeZone, or locale is null.
- * @since 3.0
- */
- public static FastDateFormat getDateTimeInstance(
- final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
- return new FastDateFormat(pattern, timeZone, locale, centuryStart);
- }
-
- // Constructor
- // -----------------------------------------------------------------------
- /**
- * <p>Constructs a new FastDateFormat.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible pattern
- * @param timeZone non-null time zone to use
- * @param locale non-null locale to use
- * @throws NullPointerException if pattern, timeZone, or locale is null.
- */
- protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) {
- this(pattern, timeZone, locale, null);
- }
-
- // Constructor
- // -----------------------------------------------------------------------
- /**
- * <p>Constructs a new FastDateFormat.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible pattern
- * @param timeZone non-null time zone to use
- * @param locale non-null locale to use
- * @param centuryStart The start of the 100 year period to use as the "default century" for 2 digit year parsing. If centuryStart is null, defaults to now - 80 years
- * @throws NullPointerException if pattern, timeZone, or locale is null.
- */
- protected FastDateFormat(
- final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
- printer = new FastDatePrinter(pattern, timeZone, locale);
- }
-
- // Format methods
- // -----------------------------------------------------------------------
- /**
- * <p>Formats a {@code Date}, {@code Calendar} or
- * {@code Long} (milliseconds) object.</p>
- * This method is an implementation of {@link Format#format(Object, StringBuilder, FieldPosition)}
- *
- * @param obj the object to format
- * @param toAppendTo the buffer to append to
- * @param pos the position - ignored
- * @return the buffer passed in
- */
- @Override
- public StringBuilder format(final Object obj, final StringBuilder toAppendTo, final FieldPosition pos) {
- return toAppendTo.append(printer.format(obj));
- }
-
- /**
- * <p>Formats a millisecond {@code long} value.</p>
- *
- * @param millis the millisecond value to format
- * @return the formatted string
- * @since 2.1
- */
- @Override
- public String format(final long millis) {
- return printer.format(millis);
- }
-
- /**
- * <p>Formats a {@code Date} object using a {@code GregorianCalendar}.</p>
- *
- * @param date the date to format
- * @return the formatted string
- */
- @Override
- public String format(final Date date) {
- return printer.format(date);
- }
-
- /**
- * <p>Formats a {@code Calendar} object.</p>
- *
- * @param calendar the calendar to format
- * @return the formatted string
- */
- @Override
- public String format(final Calendar calendar) {
- return printer.format(calendar);
- }
-
- /**
- * <p>Formats a millisecond {@code long} value into the
- * supplied {@code StringBuffer}.</p>
- *
- * @param millis the millisecond value to format
- * @param buf the buffer to format into
- * @return the specified string buffer
- * @since 3.5
- */
- @Override
- public <B extends Appendable> B format(final long millis, final B buf) {
- return printer.format(millis, buf);
- }
-
- /**
- * <p>Formats a {@code Date} object into the
- * supplied {@code StringBuffer} using a {@code GregorianCalendar}.</p>
- *
- * @param date the date to format
- * @param buf the buffer to format into
- * @return the specified string buffer
- * @since 3.5
- */
- @Override
- public <B extends Appendable> B format(final Date date, final B buf) {
- return printer.format(date, buf);
- }
-
- /**
- * <p>Formats a {@code Calendar} object into the
- * supplied {@code StringBuffer}.</p>
- *
- * @param calendar the calendar to format
- * @param buf the buffer to format into
- * @return the specified string buffer
- * @since 3.5
- */
- @Override
- public <B extends Appendable> B format(final Calendar calendar, final B buf) {
- return printer.format(calendar, buf);
- }
-
- // Accessors
- // -----------------------------------------------------------------------
- /**
- * <p>Gets the pattern used by this formatter.</p>
- *
- * @return the pattern, {@link java.text.SimpleDateFormat} compatible
- */
- @Override
- public String getPattern() {
- return printer.getPattern();
- }
-
- /**
- * <p>Gets the time zone used by this formatter.</p>
- *
- * <p>This zone is always used for {@code Date} formatting. </p>
- *
- * @return the time zone
- */
- @Override
- public TimeZone getTimeZone() {
- return printer.getTimeZone();
- }
-
- /**
- * <p>Gets the locale used by this formatter.</p>
- *
- * @return the locale
- */
- @Override
- public Locale getLocale() {
- return printer.getLocale();
- }
-
- /**
- * <p>Gets an estimate for the maximum string length that the
- * formatter will produce.</p>
- *
- * <p>The actual formatted length will almost always be less than or
- * equal to this amount.</p>
- *
- * @return the maximum formatted length
- */
- public int getMaxLengthEstimate() {
- return printer.getMaxLengthEstimate();
- }
-
- // Basics
- // -----------------------------------------------------------------------
- /**
- * <p>Compares two objects for equality.</p>
- *
- * @param obj the object to compare to
- * @return {@code true} if equal
- */
- @Override
- public boolean equals(final Object obj) {
- if (obj instanceof FastDateFormat == false) {
- return false;
- }
- final FastDateFormat other = (FastDateFormat) obj;
- // no need to check parser, as it has same invariants as printer
- return printer.equals(other.printer);
- }
-
- /**
- * <p>Returns a hash code compatible with equals.</p>
- *
- * @return a hash code compatible with equals
- */
- @Override
- public int hashCode() {
- return printer.hashCode();
- }
-
- /**
- * <p>Gets a debugging string version of this formatter.</p>
- *
- * @return a debugging string
- */
- @Override
- public String toString() {
- return "FastDateFormat[" + printer.getPattern() + "," + printer.getLocale() + ","
- + printer.getTimeZone().getID() + "]";
- }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDatePrinter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDatePrinter.java
deleted file mode 100644
index dff8e4a870..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDatePrinter.java
+++ /dev/null
@@ -1,1503 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time.internal.format;
-
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.DateFormatSymbols;
-import java.text.FieldPosition;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import org.apache.logging.log4j.core.util.Throwables;
-
-/**
- * <p>FastDatePrinter is a fast and thread-safe version of
- * {@link java.text.SimpleDateFormat}.</p>
- *
- * <p>To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}
- * or another variation of the factory methods of {@link FastDateFormat}.</p>
- *
- * <p>Since FastDatePrinter is thread safe, you can use a static member instance:</p>
- * <code>
- * private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd");
- * </code>
- *
- * <p>This class can be used as a direct replacement to
- * {@code SimpleDateFormat} in most formatting situations.
- * This class is especially useful in multi-threaded server environments.
- * {@code SimpleDateFormat} is not thread-safe in any JDK version,
- * nor will it be as Sun have closed the bug/RFE.
- * </p>
- *
- * <p>Only formatting is supported by this class, but all patterns are compatible with
- * SimpleDateFormat (except time zones and some year patterns - see below).</p>
- *
- * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
- * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
- * This pattern letter can be used here (on all JDK versions).</p>
- *
- * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
- * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}).
- * This introduces a minor incompatibility with Java 1.4, but at a gain of
- * useful functionality.</p>
- *
- * <p>Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}.
- * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using
- * one of the {@code 'X'} formats is recommended.
- *
- * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
- * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
- * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
- * 'YYY' will be formatted as '2003', while it was '03' in former Java
- * versions. FastDatePrinter implements the behavior of Java 7.</p>
- *
- * <p>
- * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
- * </p>
- *
- * @since Apache Commons Lang 3.2
- */
-public class FastDatePrinter implements DatePrinter {
- // A lot of the speed in this class comes from caching, but some comes
- // from the special int to StringBuffer conversion.
- //
- // The following produces a padded 2 digit number:
- // buffer.append((char)(value / 10 + '0'));
- // buffer.append((char)(value % 10 + '0'));
- //
- // Note that the fastest append to StringBuffer is a single char (used here).
- // Note that Integer.toString() is not called, the conversion is simply
- // taking the value and adding (mathematically) the ASCII value for '0'.
- // So, don't change this code! It works and is very fast.
-
- /**
- * FULL locale dependent date or time style.
- */
- public static final int FULL = DateFormat.FULL;
- /**
- * LONG locale dependent date or time style.
- */
- public static final int LONG = DateFormat.LONG;
- /**
- * MEDIUM locale dependent date or time style.
- */
- public static final int MEDIUM = DateFormat.MEDIUM;
- /**
- * SHORT locale dependent date or time style.
- */
- public static final int SHORT = DateFormat.SHORT;
-
- /**
- * The pattern.
- */
- private final String mPattern;
- /**
- * The time zone.
- */
- private final TimeZone mTimeZone;
- /**
- * The locale.
- */
- private final Locale mLocale;
- /**
- * The parsed rules.
- */
- private transient Rule[] mRules;
- /**
- * The estimated maximum length.
- */
- private transient int mMaxLengthEstimate;
-
- // Constructor
- // -----------------------------------------------------------------------
- /**
- * <p>Constructs a new FastDatePrinter.</p>
- * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the
- * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance.
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible pattern
- * @param timeZone non-null time zone to use
- * @param locale non-null locale to use
- * @throws NullPointerException if pattern, timeZone, or locale is null.
- */
- protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) {
- mPattern = pattern;
- mTimeZone = timeZone;
- mLocale = locale;
-
- init();
- }
-
- /**
- * <p>Initializes the instance for first use.</p>
- */
- private void init() {
- final List<Rule> rulesList = parsePattern();
- mRules = rulesList.toArray(Rule.EMPTY_ARRAY);
-
- int len = 0;
- for (int i = mRules.length; --i >= 0; ) {
- len += mRules[i].estimateLength();
- }
-
- mMaxLengthEstimate = len;
- }
-
- // Parse the pattern
- // -----------------------------------------------------------------------
- /**
- * <p>Returns a list of Rules given a pattern.</p>
- *
- * @return a {@code List} of Rule objects
- * @throws IllegalArgumentException if pattern is invalid
- */
- protected List<Rule> parsePattern() {
- final DateFormatSymbols symbols = new DateFormatSymbols(mLocale);
- final List<Rule> rules = new ArrayList<>();
-
- final String[] ERAs = symbols.getEras();
- final String[] months = symbols.getMonths();
- final String[] shortMonths = symbols.getShortMonths();
- final String[] weekdays = symbols.getWeekdays();
- final String[] shortWeekdays = symbols.getShortWeekdays();
- final String[] AmPmStrings = symbols.getAmPmStrings();
-
- final int length = mPattern.length();
- final int[] indexRef = new int[1];
-
- for (int i = 0; i < length; i++) {
- indexRef[0] = i;
- final String token = parseToken(mPattern, indexRef);
- i = indexRef[0];
-
- final int tokenLen = token.length();
- if (tokenLen == 0) {
- break;
- }
-
- Rule rule;
- final char c = token.charAt(0);
-
- switch (c) {
- case 'G': // era designator (text)
- rule = new TextField(Calendar.ERA, ERAs);
- break;
- case 'y': // year (number)
- case 'Y': // week year
- if (tokenLen == 2) {
- rule = TwoDigitYearField.INSTANCE;
- } else {
- rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen);
- }
- if (c == 'Y') {
- rule = new WeekYear((NumberRule) rule);
- }
- break;
- case 'M': // month in year (text and number)
- if (tokenLen >= 4) {
- rule = new TextField(Calendar.MONTH, months);
- } else if (tokenLen == 3) {
- rule = new TextField(Calendar.MONTH, shortMonths);
- } else if (tokenLen == 2) {
- rule = TwoDigitMonthField.INSTANCE;
- } else {
- rule = UnpaddedMonthField.INSTANCE;
- }
- break;
- case 'd': // day in month (number)
- rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);
- break;
- case 'h': // hour in am/pm (number, 1..12)
- rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));
- break;
- case 'H': // hour in day (number, 0..23)
- rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);
- break;
- case 'm': // minute in hour (number)
- rule = selectNumberRule(Calendar.MINUTE, tokenLen);
- break;
- case 's': // second in minute (number)
- rule = selectNumberRule(Calendar.SECOND, tokenLen);
- break;
- case 'S': // millisecond (number)
- rule = selectNumberRule(Calendar.MILLISECOND, tokenLen);
- break;
- case 'E': // day in week (text)
- rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays);
- break;
- case 'u': // day in week (number)
- rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen));
- break;
- case 'D': // day in year (number)
- rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);
- break;
- case 'F': // day of week in month (number)
- rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);
- break;
- case 'w': // week in year (number)
- rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);
- break;
- case 'W': // week in month (number)
- rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);
- break;
- case 'a': // am/pm marker (text)
- rule = new TextField(Calendar.AM_PM, AmPmStrings);
- break;
- case 'k': // hour in day (1..24)
- rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));
- break;
- case 'K': // hour in am/pm (0..11)
- rule = selectNumberRule(Calendar.HOUR, tokenLen);
- break;
- case 'X': // ISO 8601
- rule = Iso8601_Rule.getRule(tokenLen);
- break;
- case 'z': // time zone (text)
- if (tokenLen >= 4) {
- rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.LONG);
- } else {
- rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.SHORT);
- }
- break;
- case 'Z': // time zone (value)
- if (tokenLen == 1) {
- rule = TimeZoneNumberRule.INSTANCE_NO_COLON;
- } else if (tokenLen == 2) {
- rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;
- } else {
- rule = TimeZoneNumberRule.INSTANCE_COLON;
- }
- break;
- case '\'': // literal text
- final String sub = token.substring(1);
- if (sub.length() == 1) {
- rule = new CharacterLiteral(sub.charAt(0));
- } else {
- rule = new StringLiteral(sub);
- }
- break;
- default:
- throw new IllegalArgumentException("Illegal pattern component: " + token);
- }
-
- rules.add(rule);
- }
-
- return rules;
- }
-
- /**
- * <p>Performs the parsing of tokens.</p>
- *
- * @param pattern the pattern
- * @param indexRef index references
- * @return parsed token
- */
- protected String parseToken(final String pattern, final int[] indexRef) {
- final StringBuilder buf = new StringBuilder();
-
- int i = indexRef[0];
- final int length = pattern.length();
-
- char c = pattern.charAt(i);
- if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
- // Scan a run of the same character, which indicates a time
- // pattern.
- buf.append(c);
-
- while (i + 1 < length) {
- final char peek = pattern.charAt(i + 1);
- if (peek == c) {
- buf.append(c);
- i++;
- } else {
- break;
- }
- }
- } else {
- // This will identify token as text.
- buf.append('\'');
-
- boolean inLiteral = false;
-
- for (; i < length; i++) {
- c = pattern.charAt(i);
-
- if (c == '\'') {
- if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
- // '' is treated as escaped '
- i++;
- buf.append(c);
- } else {
- inLiteral = !inLiteral;
- }
- } else if (!inLiteral && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
- i--;
- break;
- } else {
- buf.append(c);
- }
- }
- }
-
- indexRef[0] = i;
- return buf.toString();
- }
-
- /**
- * <p>Gets an appropriate rule for the padding required.</p>
- *
- * @param field the field to get a rule for
- * @param padding the padding required
- * @return a new rule with the correct padding
- */
- protected NumberRule selectNumberRule(final int field, final int padding) {
- switch (padding) {
- case 1:
- return new UnpaddedNumberField(field);
- case 2:
- return new TwoDigitNumberField(field);
- default:
- return new PaddedNumberField(field, padding);
- }
- }
-
- // Format methods
- // -----------------------------------------------------------------------
- /**
- * <p>Formats a {@code Date}, {@code Calendar} or
- * {@code Long} (milliseconds) object.</p>
- * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}, or {{@link #format(Object)}
- * @param obj the object to format
- * @param toAppendTo the buffer to append to
- * @param pos the position - ignored
- * @return the buffer passed in
- */
- @Deprecated
- @Override
- public StringBuilder format(final Object obj, final StringBuilder toAppendTo, final FieldPosition pos) {
- if (obj instanceof Date) {
- return format((Date) obj, toAppendTo);
- } else if (obj instanceof Calendar) {
- return format((Calendar) obj, toAppendTo);
- } else if (obj instanceof Long) {
- return format(((Long) obj).longValue(), toAppendTo);
- } else {
- throw new IllegalArgumentException(
- "Unknown class: " + (obj == null ? "<null>" : obj.getClass().getName()));
- }
- }
-
- /**
- * <p>Formats a {@code Date}, {@code Calendar} or
- * {@code Long} (milliseconds) object.</p>
- * @since 3.5
- * @param obj the object to format
- * @return The formatted value.
- */
- String format(final Object obj) {
- if (obj instanceof Date) {
- return format((Date) obj);
- } else if (obj instanceof Calendar) {
- return format((Calendar) obj);
- } else if (obj instanceof Long) {
- return format(((Long) obj).longValue());
- } else {
- throw new IllegalArgumentException(
- "Unknown class: " + (obj == null ? "<null>" : obj.getClass().getName()));
- }
- }
-
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#format(long)
- */
- @Override
- public String format(final long millis) {
- final Calendar c = newCalendar();
- c.setTimeInMillis(millis);
- return applyRulesToString(c);
- }
-
- /**
- * Creates a String representation of the given Calendar by applying the rules of this printer to it.
- * @param c the Calender to apply the rules to.
- * @return a String representation of the given Calendar.
- */
- private String applyRulesToString(final Calendar c) {
- return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString();
- }
-
- /**
- * Creation method for new calender instances.
- * @return a new Calendar instance.
- */
- private Calendar newCalendar() {
- return Calendar.getInstance(mTimeZone, mLocale);
- }
-
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date)
- */
- @Override
- public String format(final Date date) {
- final Calendar c = newCalendar();
- c.setTime(date);
- return applyRulesToString(c);
- }
-
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar)
- */
- @Override
- public String format(final Calendar calendar) {
- return format(calendar, new StringBuilder(mMaxLengthEstimate)).toString();
- }
-
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.Appendable)
- */
- @Override
- public <B extends Appendable> B format(final long millis, final B buf) {
- final Calendar c = newCalendar();
- c.setTimeInMillis(millis);
- return applyRules(c, buf);
- }
-
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.Appendable)
- */
- @Override
- public <B extends Appendable> B format(final Date date, final B buf) {
- final Calendar c = newCalendar();
- c.setTime(date);
- return applyRules(c, buf);
- }
-
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.Appendable)
- */
- @Override
- public <B extends Appendable> B format(Calendar calendar, final B buf) {
- // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
- if (!calendar.getTimeZone().equals(mTimeZone)) {
- calendar = (Calendar) calendar.clone();
- calendar.setTimeZone(mTimeZone);
- }
- return applyRules(calendar, buf);
- }
-
- /**
- * <p>Performs the formatting by applying the rules to the
- * specified calendar.</p>
- *
- * @param calendar the calendar to format
- * @param buf the buffer to format into
- * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
- * @return the specified string buffer
- */
- private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) {
- try {
- for (final Rule rule : mRules) {
- rule.appendTo(buf, calendar);
- }
- } catch (final IOException ioe) {
- Throwables.rethrow(ioe);
- }
- return buf;
- }
-
- // Accessors
- // -----------------------------------------------------------------------
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#getPattern()
- */
- @Override
- public String getPattern() {
- return mPattern;
- }
-
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone()
- */
- @Override
- public TimeZone getTimeZone() {
- return mTimeZone;
- }
-
- /* (non-Javadoc)
- * @see org.apache.commons.lang3.time.DatePrinter#getLocale()
- */
- @Override
- public Locale getLocale() {
- return mLocale;
- }
-
- /**
- * <p>Gets an estimate for the maximum string length that the
- * formatter will produce.</p>
- *
- * <p>The actual formatted length will almost always be less than or
- * equal to this amount.</p>
- *
- * @return the maximum formatted length
- */
- public int getMaxLengthEstimate() {
- return mMaxLengthEstimate;
- }
-
- // Basics
- // -----------------------------------------------------------------------
- /**
- * <p>Compares two objects for equality.</p>
- *
- * @param obj the object to compare to
- * @return {@code true} if equal
- */
- @Override
- public boolean equals(final Object obj) {
- if (!(obj instanceof FastDatePrinter)) {
- return false;
- }
- final FastDatePrinter other = (FastDatePrinter) obj;
- return mPattern.equals(other.mPattern) && mTimeZone.equals(other.mTimeZone) && mLocale.equals(other.mLocale);
- }
-
- /**
- * <p>Returns a hash code compatible with equals.</p>
- *
- * @return a hash code compatible with equals
- */
- @Override
- public int hashCode() {
- return mPattern.hashCode() + 13 * (mTimeZone.hashCode() + 13 * mLocale.hashCode());
- }
-
- /**
- * <p>Gets a debugging string version of this formatter.</p>
- *
- * @return a debugging string
- */
- @Override
- public String toString() {
- return "FastDatePrinter[" + mPattern + "," + mLocale + "," + mTimeZone.getID() + "]";
- }
-
- /**
- * Appends two digits to the given buffer.
- *
- * @param buffer the buffer to append to.
- * @param value the value to append digits from.
- */
- private static void appendDigits(final Appendable buffer, final int value) throws IOException {
- buffer.append((char) (value / 10 + '0'));
- buffer.append((char) (value % 10 + '0'));
- }
-
- private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3
-
- /**
- * Appends all digits to the given buffer.
- *
- * @param buffer the buffer to append to.
- * @param value the value to append digits from.
- */
- private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException {
- // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array
- // see LANG-1248
- if (value < 10000) {
- // less memory allocation path works for four digits or less
-
- int nDigits = 4;
- if (value < 1000) {
- --nDigits;
- if (value < 100) {
- --nDigits;
- if (value < 10) {
- --nDigits;
- }
- }
- }
- // left zero pad
- for (int i = minFieldWidth - nDigits; i > 0; --i) {
- buffer.append('0');
- }
-
- switch (nDigits) {
- case 4:
- buffer.append((char) (value / 1000 + '0'));
- value %= 1000;
- case 3:
- if (value >= 100) {
- buffer.append((char) (value / 100 + '0'));
- value %= 100;
- } else {
- buffer.append('0');
- }
- case 2:
- if (value >= 10) {
- buffer.append((char) (value / 10 + '0'));
- value %= 10;
- } else {
- buffer.append('0');
- }
- case 1:
- buffer.append((char) (value + '0'));
- }
- } else {
- // more memory allocation path works for any digits
-
- // build up decimal representation in reverse
- final char[] work = new char[MAX_DIGITS];
- int digit = 0;
- while (value != 0) {
- work[digit++] = (char) (value % 10 + '0');
- value = value / 10;
- }
-
- // pad with zeros
- while (digit < minFieldWidth) {
- buffer.append('0');
- --minFieldWidth;
- }
-
- // reverse
- while (--digit >= 0) {
- buffer.append(work[digit]);
- }
- }
- }
-
- // Rules
- // -----------------------------------------------------------------------
- /**
- * <p>Inner class defining a rule.</p>
- */
- private interface Rule {
-
- Rule[] EMPTY_ARRAY = {};
-
- /**
- * Returns the estimated length of the result.
- *
- * @return the estimated length
- */
- int estimateLength();
-
- /**
- * Appends the value of the specified calendar to the output buffer based on the rule implementation.
- *
- * @param buf the output buffer
- * @param calendar calendar to be appended
- * @throws IOException if an I/O error occurs
- */
- void appendTo(Appendable buf, Calendar calendar) throws IOException;
- }
-
- /**
- * <p>Inner class defining a numeric rule.</p>
- */
- private interface NumberRule extends Rule {
- /**
- * Appends the specified value to the output buffer based on the rule implementation.
- *
- * @param buffer the output buffer
- * @param value the value to be appended
- * @throws IOException if an I/O error occurs
- */
- void appendTo(Appendable buffer, int value) throws IOException;
- }
-
- /**
- * <p>Inner class to output a constant single character.</p>
- */
- private static class CharacterLiteral implements Rule {
- private final char mValue;
-
- /**
- * Constructs a new instance of {@code CharacterLiteral}
- * to hold the specified value.
- *
- * @param value the character literal
- */
- CharacterLiteral(final char value) {
- mValue = value;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return 1;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- buffer.append(mValue);
- }
- }
-
- /**
- * <p>Inner class to output a constant string.</p>
- */
- private static class StringLiteral implements Rule {
- private final String mValue;
-
- /**
- * Constructs a new instance of {@code StringLiteral}
- * to hold the specified value.
- *
- * @param value the string literal
- */
- StringLiteral(final String value) {
- mValue = value;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return mValue.length();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- buffer.append(mValue);
- }
- }
-
- /**
- * <p>Inner class to output one of a set of values.</p>
- */
- private static class TextField implements Rule {
- private final int mField;
- private final String[] mValues;
-
- /**
- * Constructs an instance of {@code TextField}
- * with the specified field and values.
- *
- * @param field the field
- * @param values the field values
- */
- TextField(final int field, final String[] values) {
- mField = field;
- mValues = values;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- int max = 0;
- for (int i = mValues.length; --i >= 0; ) {
- final int len = mValues[i].length();
- if (len > max) {
- max = len;
- }
- }
- return max;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- buffer.append(mValues[calendar.get(mField)]);
- }
- }
-
- /**
- * <p>Inner class to output an unpadded number.</p>
- */
- private static class UnpaddedNumberField implements NumberRule {
- private final int mField;
-
- /**
- * Constructs an instance of {@code UnpadedNumberField} with the specified field.
- *
- * @param field the field
- */
- UnpaddedNumberField(final int field) {
- mField = field;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return 4;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- appendTo(buffer, calendar.get(mField));
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public final void appendTo(final Appendable buffer, final int value) throws IOException {
- if (value < 10) {
- buffer.append((char) (value + '0'));
- } else if (value < 100) {
- appendDigits(buffer, value);
- } else {
- appendFullDigits(buffer, value, 1);
- }
- }
- }
-
- /**
- * <p>Inner class to output an unpadded month.</p>
- */
- private static class UnpaddedMonthField implements NumberRule {
- static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField();
-
- /**
- * Constructs an instance of {@code UnpaddedMonthField}.
- *
- */
- UnpaddedMonthField() {
- super();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return 2;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public final void appendTo(final Appendable buffer, final int value) throws IOException {
- if (value < 10) {
- buffer.append((char) (value + '0'));
- } else {
- appendDigits(buffer, value);
- }
- }
- }
-
- /**
- * <p>Inner class to output a padded number.</p>
- */
- private static class PaddedNumberField implements NumberRule {
- private final int mField;
- private final int mSize;
-
- /**
- * Constructs an instance of {@code PaddedNumberField}.
- *
- * @param field the field
- * @param size size of the output field
- */
- PaddedNumberField(final int field, final int size) {
- if (size < 3) {
- // Should use UnpaddedNumberField or TwoDigitNumberField.
- throw new IllegalArgumentException();
- }
- mField = field;
- mSize = size;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return mSize;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- appendTo(buffer, calendar.get(mField));
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public final void appendTo(final Appendable buffer, final int value) throws IOException {
- appendFullDigits(buffer, value, mSize);
- }
- }
-
- /**
- * <p>Inner class to output a two digit number.</p>
- */
- private static class TwoDigitNumberField implements NumberRule {
- private final int mField;
-
- /**
- * Constructs an instance of {@code TwoDigitNumberField} with the specified field.
- *
- * @param field the field
- */
- TwoDigitNumberField(final int field) {
- mField = field;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return 2;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- appendTo(buffer, calendar.get(mField));
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public final void appendTo(final Appendable buffer, final int value) throws IOException {
- if (value < 100) {
- appendDigits(buffer, value);
- } else {
- appendFullDigits(buffer, value, 2);
- }
- }
- }
-
- /**
- * <p>Inner class to output a two digit year.</p>
- */
- private static class TwoDigitYearField implements NumberRule {
- static final TwoDigitYearField INSTANCE = new TwoDigitYearField();
-
- /**
- * Constructs an instance of {@code TwoDigitYearField}.
- */
- TwoDigitYearField() {
- super();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return 2;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- appendTo(buffer, calendar.get(Calendar.YEAR) % 100);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public final void appendTo(final Appendable buffer, final int value) throws IOException {
- appendDigits(buffer, value);
- }
- }
-
- /**
- * <p>Inner class to output a two digit month.</p>
- */
- private static class TwoDigitMonthField implements NumberRule {
- static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField();
-
- /**
- * Constructs an instance of {@code TwoDigitMonthField}.
- */
- TwoDigitMonthField() {
- super();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return 2;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public final void appendTo(final Appendable buffer, final int value) throws IOException {
- appendDigits(buffer, value);
- }
- }
-
- /**
- * <p>Inner class to output the twelve hour field.</p>
- */
- private static class TwelveHourField implements NumberRule {
- private final NumberRule mRule;
-
- /**
- * Constructs an instance of {@code TwelveHourField} with the specified
- * {@code NumberRule}.
- *
- * @param rule the rule
- */
- TwelveHourField(final NumberRule rule) {
- mRule = rule;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return mRule.estimateLength();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- int value = calendar.get(Calendar.HOUR);
- if (value == 0) {
- value = calendar.getLeastMaximum(Calendar.HOUR) + 1;
- }
- mRule.appendTo(buffer, value);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final int value) throws IOException {
- mRule.appendTo(buffer, value);
- }
- }
-
- /**
- * <p>Inner class to output the twenty four hour field.</p>
- */
- private static class TwentyFourHourField implements NumberRule {
- private final NumberRule mRule;
-
- /**
- * Constructs an instance of {@code TwentyFourHourField} with the specified
- * {@code NumberRule}.
- *
- * @param rule the rule
- */
- TwentyFourHourField(final NumberRule rule) {
- mRule = rule;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return mRule.estimateLength();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- int value = calendar.get(Calendar.HOUR_OF_DAY);
- if (value == 0) {
- value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1;
- }
- mRule.appendTo(buffer, value);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final int value) throws IOException {
- mRule.appendTo(buffer, value);
- }
- }
-
- /**
- * <p>Inner class to output the numeric day in week.</p>
- */
- private static class DayInWeekField implements NumberRule {
- private final NumberRule mRule;
-
- DayInWeekField(final NumberRule rule) {
- mRule = rule;
- }
-
- @Override
- public int estimateLength() {
- return mRule.estimateLength();
- }
-
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- final int value = calendar.get(Calendar.DAY_OF_WEEK);
- mRule.appendTo(buffer, value != Calendar.SUNDAY ? value - 1 : 7);
- }
-
- @Override
- public void appendTo(final Appendable buffer, final int value) throws IOException {
- mRule.appendTo(buffer, value);
- }
- }
-
- /**
- * <p>Inner class to output the numeric day in week.</p>
- */
- private static class WeekYear implements NumberRule {
- private final NumberRule mRule;
-
- WeekYear(final NumberRule rule) {
- mRule = rule;
- }
-
- @Override
- public int estimateLength() {
- return mRule.estimateLength();
- }
-
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- mRule.appendTo(buffer, calendar.getWeekYear());
- }
-
- @Override
- public void appendTo(final Appendable buffer, final int value) throws IOException {
- mRule.appendTo(buffer, value);
- }
- }
-
- // -----------------------------------------------------------------------
-
- private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache = new ConcurrentHashMap<>(7);
- /**
- * <p>Gets the time zone display name, using a cache for performance.</p>
- *
- * @param tz the zone to query
- * @param daylight true if daylight savings
- * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT}
- * @param locale the locale to use
- * @return the textual name of the time zone
- */
- static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) {
- final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale);
- String value = cTimeZoneDisplayCache.get(key);
- if (value == null) {
- // This is a very slow call, so cache the results.
- value = tz.getDisplayName(daylight, style, locale);
- final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value);
- if (prior != null) {
- value = prior;
- }
- }
- return value;
- }
-
- /**
- * <p>Inner class to output a time zone name.</p>
- */
- private static class TimeZoneNameRule implements Rule {
- private final Locale mLocale;
- private final int mStyle;
- private final String mStandard;
- private final String mDaylight;
-
- /**
- * Constructs an instance of {@code TimeZoneNameRule} with the specified properties.
- *
- * @param timeZone the time zone
- * @param locale the locale
- * @param style the style
- */
- TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) {
- mLocale = locale;
- mStyle = style;
-
- mStandard = getTimeZoneDisplay(timeZone, false, style, locale);
- mDaylight = getTimeZoneDisplay(timeZone, true, style, locale);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- // We have no access to the Calendar object that will be passed to
- // appendTo so base estimate on the TimeZone passed to the
- // constructor
- return Math.max(mStandard.length(), mDaylight.length());
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- final TimeZone zone = calendar.getTimeZone();
- if (calendar.get(Calendar.DST_OFFSET) != 0) {
- buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale));
- } else {
- buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale));
- }
- }
- }
-
- /**
- * <p>Inner class to output a time zone as a number {@code +/-HHMM}
- * or {@code +/-HH:MM}.</p>
- */
- private static class TimeZoneNumberRule implements Rule {
- static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true);
- static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false);
-
- final boolean mColon;
-
- /**
- * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties.
- *
- * @param colon add colon between HH and MM in the output if {@code true}
- */
- TimeZoneNumberRule(final boolean colon) {
- mColon = colon;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return 5;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-
- int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
-
- if (offset < 0) {
- buffer.append('-');
- offset = -offset;
- } else {
- buffer.append('+');
- }
-
- final int hours = offset / (60 * 60 * 1000);
- appendDigits(buffer, hours);
-
- if (mColon) {
- buffer.append(':');
- }
-
- final int minutes = offset / (60 * 1000) - 60 * hours;
- appendDigits(buffer, minutes);
- }
- }
-
- /**
- * <p>Inner class to output a time zone as a number {@code +/-HHMM}
- * or {@code +/-HH:MM}.</p>
- */
- private static class Iso8601_Rule implements Rule {
-
- // Sign TwoDigitHours or Z
- static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3);
- // Sign TwoDigitHours Minutes or Z
- static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5);
- // Sign TwoDigitHours : Minutes or Z
- static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6);
-
- /**
- * Factory method for Iso8601_Rules.
- *
- * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
- * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such
- * rule exists, an IllegalArgumentException will be thrown.
- */
- static Iso8601_Rule getRule(final int tokenLen) {
- switch (tokenLen) {
- case 1:
- return Iso8601_Rule.ISO8601_HOURS;
- case 2:
- return Iso8601_Rule.ISO8601_HOURS_MINUTES;
- case 3:
- return Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;
- default:
- throw new IllegalArgumentException("invalid number of X");
- }
- }
-
- final int length;
-
- /**
- * Constructs an instance of {@code Iso8601_Rule} with the specified properties.
- *
- * @param length The number of characters in output (unless Z is output)
- */
- Iso8601_Rule(final int length) {
- this.length = length;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int estimateLength() {
- return length;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
- int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
- if (offset == 0) {
- buffer.append("Z");
- return;
- }
-
- if (offset < 0) {
- buffer.append('-');
- offset = -offset;
- } else {
- buffer.append('+');
- }
-
- final int hours = offset / (60 * 60 * 1000);
- appendDigits(buffer, hours);
-
- if (length < 5) {
- return;
- }
-
- if (length == 6) {
- buffer.append(':');
- }
-
- final int minutes = offset / (60 * 1000) - 60 * hours;
- appendDigits(buffer, minutes);
- }
- }
-
- // ----------------------------------------------------------------------
- /**
- * <p>Inner class that acts as a compound key for time zone names.</p>
- */
- private static class TimeZoneDisplayKey {
- private final TimeZone mTimeZone;
- private final int mStyle;
- private final Locale mLocale;
-
- /**
- * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties.
- *
- * @param timeZone the time zone
- * @param daylight adjust the style for daylight saving time if {@code true}
- * @param style the timezone style
- * @param locale the timezone locale
- */
- TimeZoneDisplayKey(final TimeZone timeZone, final boolean daylight, final int style, final Locale locale) {
- mTimeZone = timeZone;
- if (daylight) {
- mStyle = style | 0x80000000;
- } else {
- mStyle = style;
- }
- mLocale = locale;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int hashCode() {
- return (mStyle * 31 + mLocale.hashCode()) * 31 + mTimeZone.hashCode();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean equals(final Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj instanceof TimeZoneDisplayKey) {
- final TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj;
- return mTimeZone.equals(other.mTimeZone) && mStyle == other.mStyle && mLocale.equals(other.mLocale);
- }
- return false;
- }
- }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java
deleted file mode 100644
index b2986151b7..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java
+++ /dev/null
@@ -1,760 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time.internal.format;
-
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Objects;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import org.apache.logging.log4j.core.time.Instant;
-import org.osgi.annotation.versioning.ProviderType;
-
-/**
- * Custom time formatter that trades flexibility for performance. This formatter only supports the date patterns defined
- * in {@link FixedFormat}. For any other date patterns use {@link FastDateFormat}.
- * <p>
- * Related benchmarks: /log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and
- * /log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
- * </p>
- */
-@ProviderType
-public class FixedDateFormat {
-
- /**
- * Enumeration over the supported date/time format patterns.
- * <p>
- * Package protected for unit tests.
- * </p>
- */
- public enum FixedFormat {
- /**
- * ABSOLUTE time format: {@code "HH:mm:ss,SSS"}.
- */
- ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1, 3, null),
- /**
- * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,nnnnnn"}.
- */
- ABSOLUTE_MICROS("HH:mm:ss,nnnnnn", null, 0, ':', 1, ',', 1, 6, null),
- /**
- * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,nnnnnnnnn"}.
- */
- ABSOLUTE_NANOS("HH:mm:ss,nnnnnnnnn", null, 0, ':', 1, ',', 1, 9, null),
-
- /**
- * ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}.
- */
- ABSOLUTE_PERIOD("HH:mm:ss.SSS", null, 0, ':', 1, '.', 1, 3, null),
-
- /**
- * COMPACT time format: {@code "yyyyMMddHHmmssSSS"}.
- */
- COMPACT("yyyyMMddHHmmssSSS", "yyyyMMdd", 0, ' ', 0, ' ', 0, 3, null),
-
- /**
- * DATE_AND_TIME time format: {@code "dd MMM yyyy HH:mm:ss,SSS"}.
- */
- DATE("dd MMM yyyy HH:mm:ss,SSS", "dd MMM yyyy ", 0, ':', 1, ',', 1, 3, null),
-
- /**
- * DATE_AND_TIME time format variation with period separator: {@code "dd MMM yyyy HH:mm:ss.SSS"}.
- */
- DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS", "dd MMM yyyy ", 0, ':', 1, '.', 1, 3, null),
-
- /**
- * DEFAULT time format: {@code "yyyy-MM-dd HH:mm:ss,SSS"}.
- */
- DEFAULT("yyyy-MM-dd HH:mm:ss,SSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 3, null),
- /**
- * DEFAULT time format with microsecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnn"}.
- */
- DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,nnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 6, null),
- /**
- * DEFAULT time format with nanosecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"}.
- */
- DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,nnnnnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 9, null),
-
- /**
- * DEFAULT time format variation with period separator: {@code "yyyy-MM-dd HH:mm:ss.SSS"}.
- */
- DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS", "yyyy-MM-dd ", 0, ':', 1, '.', 1, 3, null),
-
- /**
- * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss,SSS"}.
- */
- ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS", "yyyyMMdd'T'", 2, ' ', 0, ',', 1, 3, null),
-
- /**
- * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss.SSS"}.
- */
- ISO8601_BASIC_PERIOD("yyyyMMdd'T'HHmmss.SSS", "yyyyMMdd'T'", 2, ' ', 0, '.', 1, 3, null),
-
- /**
- * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSS"}.
- */
- ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, null),
-
- // TODO Do we even want a format without seconds?
- // /**
- // * ISO8601_OFFSET_DATE_TIME time format: {@code "yyyy-MM-dd'T'HH:mmXXX"}.
- // */
- // // Would need work in org.apache.logging.log4j.core.util.datetime.FixedDateFormat.writeTime(int, char[],
- // int)
- // ISO8601_OFFSET_DATE_TIME("yyyy-MM-dd'T'HH:mmXXX", "yyyy-MM-dd'T'", 2, ':', 1, ' ', 0, 0,
- // FixedTimeZoneFormat.XXX),
-
- /**
- * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSX"} with a time zone like {@code -07}.
- */
- ISO8601_OFFSET_DATE_TIME_HH(
- "yyyy-MM-dd'T'HH:mm:ss,SSSX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.HH),
-
- /**
- * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSXX"} with a time zone like {@code -0700}.
- */
- ISO8601_OFFSET_DATE_TIME_HHMM(
- "yyyy-MM-dd'T'HH:mm:ss,SSSXX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.HHMM),
-
- /**
- * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSXXX"} with a time zone like {@code -07:00}.
- */
- ISO8601_OFFSET_DATE_TIME_HHCMM(
- "yyyy-MM-dd'T'HH:mm:ss,SSSXXX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.HHCMM),
-
- /**
- * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss.SSS"}.
- */
- ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 3, null),
-
- /**
- * ISO8601 time format with support for microsecond precision: {@code "yyyy-MM-dd'T'HH:mm:ss.nnnnnn"}.
- */
- ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.nnnnnn", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 6, null),
-
- /**
- * American date/time format with 2-digit year: {@code "dd/MM/yy HH:mm:ss.SSS"}.
- */
- US_MONTH_DAY_YEAR2_TIME("dd/MM/yy HH:mm:ss.SSS", "dd/MM/yy ", 0, ':', 1, '.', 1, 3, null),
-
- /**
- * American date/time format with 4-digit year: {@code "dd/MM/yyyy HH:mm:ss.SSS"}.
- */
- US_MONTH_DAY_YEAR4_TIME("dd/MM/yyyy HH:mm:ss.SSS", "dd/MM/yyyy ", 0, ':', 1, '.', 1, 3, null);
-
- private static final String DEFAULT_SECOND_FRACTION_PATTERN = "SSS";
- private static final int MILLI_FRACTION_DIGITS = DEFAULT_SECOND_FRACTION_PATTERN.length();
- private static final char SECOND_FRACTION_PATTERN = 'n';
-
- private final String pattern;
- private final String datePattern;
- private final int escapeCount;
- private final char timeSeparatorChar;
- private final int timeSeparatorLength;
- private final char millisSeparatorChar;
- private final int millisSeparatorLength;
- private final int secondFractionDigits;
- private final FixedTimeZoneFormat fixedTimeZoneFormat;
-
- FixedFormat(
- final String pattern,
- final String datePattern,
- final int escapeCount,
- final char timeSeparator,
- final int timeSepLength,
- final char millisSeparator,
- final int millisSepLength,
- final int secondFractionDigits,
- final FixedTimeZoneFormat fixedTimeZoneFormat) {
- this.timeSeparatorChar = timeSeparator;
- this.timeSeparatorLength = timeSepLength;
- this.millisSeparatorChar = millisSeparator;
- this.millisSeparatorLength = millisSepLength;
- this.pattern = Objects.requireNonNull(pattern);
- this.datePattern = datePattern; // may be null
- this.escapeCount = escapeCount;
- this.secondFractionDigits = secondFractionDigits;
- this.fixedTimeZoneFormat = fixedTimeZoneFormat;
- }
-
- /**
- * Returns the full pattern.
- *
- * @return the full pattern
- */
- public String getPattern() {
- return pattern;
- }
-
- /**
- * Returns the date part of the pattern.
- *
- * @return the date part of the pattern
- */
- public String getDatePattern() {
- return datePattern;
- }
-
- /**
- * Returns the FixedFormat with the name or pattern matching the specified string or {@code null} if not found.
- *
- * @param nameOrPattern the name or pattern to find a FixedFormat for
- * @return the FixedFormat with the name or pattern matching the specified string
- */
- public static FixedFormat lookup(final String nameOrPattern) {
- for (final FixedFormat type : FixedFormat.values()) {
- if (type.name().equals(nameOrPattern) || type.getPattern().equals(nameOrPattern)) {
- return type;
- }
- }
- return null;
- }
-
- static FixedFormat lookupIgnoringNanos(final String pattern) {
- final int[] nanoRange = nanoRange(pattern);
- final int nanoStart = nanoRange[0];
- final int nanoEnd = nanoRange[1];
- if (nanoStart > 0) {
- final String subPattern = pattern.substring(0, nanoStart)
- + DEFAULT_SECOND_FRACTION_PATTERN
- + pattern.substring(nanoEnd, pattern.length());
- for (final FixedFormat type : FixedFormat.values()) {
- if (type.getPattern().equals(subPattern)) {
- return type;
- }
- }
- }
- return null;
- }
-
- private static final int[] EMPTY_RANGE = {-1, -1};
-
- /**
- * @return int[0] start index inclusive; int[1] end index exclusive
- */
- private static int[] nanoRange(final String pattern) {
- final int indexStart = pattern.indexOf(SECOND_FRACTION_PATTERN);
- int indexEnd = -1;
- if (indexStart >= 0) {
- indexEnd = pattern.indexOf('Z', indexStart);
- indexEnd = indexEnd < 0 ? pattern.indexOf('X', indexStart) : indexEnd;
- indexEnd = indexEnd < 0 ? pattern.length() : indexEnd;
- for (int i = indexStart + 1; i < indexEnd; i++) {
- if (pattern.charAt(i) != SECOND_FRACTION_PATTERN) {
- return EMPTY_RANGE;
- }
- }
- }
- return new int[] {indexStart, indexEnd};
- }
-
- /**
- * Returns the optional time zone format.
- * @return the optional time zone format, may be null.
- */
- public FixedTimeZoneFormat getTimeZoneFormat() {
- return fixedTimeZoneFormat;
- }
-
- /**
- * Returns the length of the resulting formatted date and time strings.
- *
- * @return the length of the resulting formatted date and time strings
- */
- public int getLength() {
- return pattern.length() - escapeCount;
- }
-
- /**
- * Returns the length of the date part of the resulting formatted string.
- *
- * @return the length of the date part of the resulting formatted string
- */
- public int getDatePatternLength() {
- return getDatePattern() == null ? 0 : getDatePattern().length() - escapeCount;
- }
-
- /**
- * Returns the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null} if the
- * pattern does not have a date part.
- *
- * @return the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null}
- */
- public FastDateFormat getFastDateFormat() {
- return getFastDateFormat(null);
- }
-
- /**
- * Returns the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null} if the
- * pattern does not have a date part.
- *
- * @param tz the time zone to use
- * @return the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null}
- */
- public FastDateFormat getFastDateFormat(final TimeZone tz) {
- return getDatePattern() == null ? null : FastDateFormat.getInstance(getDatePattern(), tz);
- }
-
- /**
- * Returns the number of digits specifying the fraction of the second to show
- * @return 3 for millisecond precision, 6 for microsecond precision or 9 for nanosecond precision
- */
- public int getSecondFractionDigits() {
- return secondFractionDigits;
- }
- }
-
- private static final char NONE = (char) 0;
-
- /**
- * Fixed time zone formats. The enum names are symbols from Java's <a href=
- * "https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html">DateTimeFormatter</a>.
- *
- * @see <a href=
- * "https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html">DateTimeFormatter</a>
- */
- public enum FixedTimeZoneFormat {
-
- /**
- * Offset like {@code -07}
- */
- HH(NONE, false, 3),
-
- /**
- * Offset like {@code -0700}.
- * Same as Z.
- */
- HHMM(NONE, true, 5),
-
- /**
- * Offset like {@code -07:00}
- */
- HHCMM(':', true, 6);
-
- FixedTimeZoneFormat() {
- this(NONE, true, 4);
- }
-
- FixedTimeZoneFormat(final char timeSeparatorChar, final boolean minutes, final int length) {
- this.timeSeparatorChar = timeSeparatorChar;
- this.timeSeparatorCharLen = timeSeparatorChar != NONE ? 1 : 0;
- this.useMinutes = minutes;
- this.length = length;
- }
-
- private final char timeSeparatorChar;
- private final int timeSeparatorCharLen;
- private final boolean useMinutes;
- // The length includes 1 for the leading sign
- private final int length;
-
- public int getLength() {
- return length;
- }
-
- // Profiling showed this method is important to log4j performance. Modify with care!
- // 262 bytes (will be inlined when hot enough: <= -XX:FreqInlineSize=325 bytes on Linux)
- private int write(final int offset, final char[] buffer, final int pos) {
- // This method duplicates part of writeTime()
- int p = pos;
- buffer[p++] = offset < 0 ? '-' : '+';
- final int absOffset = Math.abs(offset);
- final int hours = absOffset / 3600000;
- int ms = absOffset - (3600000 * hours);
-
- // Hour
- int temp = hours / 10;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (hours - 10 * temp + '0'));
-
- // Minute
- if (useMinutes) {
- buffer[p] = timeSeparatorChar;
- p += timeSeparatorCharLen;
- final int minutes = ms / 60000;
- ms -= 60000 * minutes;
-
- temp = minutes / 10;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (minutes - 10 * temp + '0'));
- }
- return p;
- }
- }
-
- private final FixedFormat fixedFormat;
- private final TimeZone timeZone;
- private final int length;
- private final int secondFractionDigits;
- private final FastDateFormat fastDateFormat; // may be null
- private final char timeSeparatorChar;
- private final char millisSeparatorChar;
- private final int timeSeparatorLength;
- private final int millisSeparatorLength;
- private final FixedTimeZoneFormat fixedTimeZoneFormat;
-
- private volatile long midnightToday;
- private volatile long midnightTomorrow;
- private final int[] dstOffsets = new int[25];
-
- // cachedDate does not need to be volatile because
- // there is a write to a volatile field *after* cachedDate is modified,
- // and there is a read from a volatile field *before* cachedDate is read.
- // The Java memory model guarantees that because of the above,
- // changes to cachedDate in one thread are visible to other threads.
- // See http://g.oswego.edu/dl/jmm/cookbook.html
- private char[] cachedDate; // may be null
- private int dateLength;
- private final Lock cacheLock = new ReentrantLock();
-
- /**
- * Constructs a FixedDateFormat for the specified fixed format.
- * <p>
- * Package protected for unit tests.
- * </p>
- *
- * @param fixedFormat the fixed format
- * @param tz time zone
- */
- FixedDateFormat(final FixedFormat fixedFormat, final TimeZone tz) {
- this(fixedFormat, tz, fixedFormat.getSecondFractionDigits());
- }
-
- /**
- * Constructs a FixedDateFormat for the specified fixed format.
- * <p>
- * Package protected for unit tests.
- * </p>
- *
- * @param fixedFormat the fixed format
- * @param tz time zone
- * @param secondFractionDigits the number of digits specifying the fraction of the second to show
- */
- FixedDateFormat(final FixedFormat fixedFormat, final TimeZone tz, final int secondFractionDigits) {
- this.fixedFormat = Objects.requireNonNull(fixedFormat);
- this.timeZone = Objects.requireNonNull(tz);
- this.timeSeparatorChar = fixedFormat.timeSeparatorChar;
- this.timeSeparatorLength = fixedFormat.timeSeparatorLength;
- this.millisSeparatorChar = fixedFormat.millisSeparatorChar;
- this.millisSeparatorLength = fixedFormat.millisSeparatorLength;
- this.fixedTimeZoneFormat = fixedFormat.fixedTimeZoneFormat;
- this.length = fixedFormat.getLength();
- this.secondFractionDigits = Math.max(1, Math.min(9, secondFractionDigits));
- this.fastDateFormat = fixedFormat.getFastDateFormat(tz);
- }
-
- public static FixedDateFormat createIfSupported(final String... options) {
- if (options == null || options.length == 0 || options[0] == null) {
- return new FixedDateFormat(FixedFormat.DEFAULT, TimeZone.getDefault());
- }
- final TimeZone tz;
- if (options.length > 1) {
- if (options[1] != null) {
- String zoneId = options[1];
- if (zoneId.startsWith("-") || zoneId.startsWith("+")) {
- zoneId = "GMT" + zoneId;
- }
- tz = TimeZone.getTimeZone(zoneId);
- } else {
- tz = TimeZone.getDefault();
- }
- } else {
- tz = TimeZone.getDefault();
- }
-
- final String option0 = options[0];
- final FixedFormat withoutNanos = FixedFormat.lookupIgnoringNanos(option0);
- if (withoutNanos != null) {
- final int[] nanoRange = FixedFormat.nanoRange(option0);
- final int nanoStart = nanoRange[0];
- final int nanoEnd = nanoRange[1];
- final int secondFractionDigits = nanoEnd - nanoStart;
- return new FixedDateFormat(withoutNanos, tz, secondFractionDigits);
- }
- final FixedFormat type = FixedFormat.lookup(option0);
- return type == null ? null : new FixedDateFormat(type, tz);
- }
-
- /**
- * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and a {@code TimeZone.getDefault()} TimeZone.
- *
- * @param format the format to use
- * @return a new {@code FixedDateFormat} object
- */
- public static FixedDateFormat create(final FixedFormat format) {
- return new FixedDateFormat(format, TimeZone.getDefault());
- }
-
- /**
- * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and TimeZone.
- *
- * @param format the format to use
- * @param tz the time zone to use
- * @return a new {@code FixedDateFormat} object
- */
- public static FixedDateFormat create(final FixedFormat format, final TimeZone tz) {
- return new FixedDateFormat(format, tz != null ? tz : TimeZone.getDefault());
- }
-
- /**
- * Returns the full pattern of the selected fixed format.
- *
- * @return the full date-time pattern
- */
- public String getFormat() {
- return fixedFormat.getPattern();
- }
-
- /**
- * Returns the length of the resulting formatted date and time strings.
- *
- * @return the length of the resulting formatted date and time strings
- */
- public final int getLength() {
- return length;
- }
-
- /**
- * Returns the time zone.
- *
- * @return the time zone
- */
- public TimeZone getTimeZone() {
- return timeZone;
- }
-
- /**
- * <p>Returns the number of milliseconds since midnight in the time zone that this {@code FixedDateFormat}
- * was constructed with for the specified currentTime.</p>
- * <p>As a side effect, this method updates the cached formatted date and the cached date demarcation timestamps
- * when the specified current time is outside the previously set demarcation timestamps for the start or end
- * of the current day.</p>
- * @param currentTime the current time in millis since the epoch
- * @return the number of milliseconds since midnight for the specified time
- */
- // Profiling showed this method is important to log4j performance. Modify with care!
- // 30 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
- public long millisSinceMidnight(final long currentTime) {
- if (currentTime >= midnightTomorrow || currentTime < midnightToday) {
- updateMidnightMillis(currentTime);
- }
- return currentTime - midnightToday;
- }
-
- private void updateMidnightMillis(final long now) {
- if (now >= midnightTomorrow || now < midnightToday) {
- cacheLock.lock();
- try {
- updateCachedDate(now);
- midnightToday = calcMidnightMillis(now, 0);
- midnightTomorrow = calcMidnightMillis(now, 1);
-
- updateDaylightSavingTime();
- } finally {
- cacheLock.unlock();
- }
- }
- }
-
- private long calcMidnightMillis(final long time, final int addDays) {
- final Calendar cal = Calendar.getInstance(timeZone);
- cal.setTimeInMillis(time);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- cal.add(Calendar.DATE, addDays);
- return cal.getTimeInMillis();
- }
-
- private void updateDaylightSavingTime() {
- Arrays.fill(dstOffsets, 0);
- final int ONE_HOUR = (int) TimeUnit.HOURS.toMillis(1);
- if (timeZone.getOffset(midnightToday) != timeZone.getOffset(midnightToday + 23 * ONE_HOUR)) {
- for (int i = 0; i < dstOffsets.length; i++) {
- final long time = midnightToday + i * ONE_HOUR;
- dstOffsets[i] = timeZone.getOffset(time) - timeZone.getRawOffset();
- }
- if (dstOffsets[0] > dstOffsets[23]) { // clock is moved backwards.
- // we obtain midnightTonight with Calendar.getInstance(TimeZone), so it already includes raw offset
- for (int i = dstOffsets.length - 1; i >= 0; i--) {
- dstOffsets[i] -= dstOffsets[0]; //
- }
- }
- }
- }
-
- private void updateCachedDate(final long now) {
- if (fastDateFormat != null) {
- final StringBuilder result = fastDateFormat.format(now, new StringBuilder());
- cachedDate = result.toString().toCharArray();
- dateLength = result.length();
- }
- }
-
- public String formatInstant(final Instant instant) {
- final char[] result = new char[length << 1]; // double size for locales with lengthy DateFormatSymbols
- final int written = formatInstant(instant, result, 0);
- return new String(result, 0, written);
- }
-
- public int formatInstant(final Instant instant, final char[] buffer, final int startPos) {
- final long epochMillisecond = instant.getEpochMillisecond();
- int result = format(epochMillisecond, buffer, startPos);
- result -= digitsLessThanThree();
- final int pos = formatNanoOfMillisecond(instant.getNanoOfMillisecond(), buffer, startPos + result);
- return writeTimeZone(epochMillisecond, buffer, pos);
- }
-
- private int digitsLessThanThree() { // in case user specified only 1 or 2 'n' format characters
- return Math.max(0, FixedFormat.MILLI_FRACTION_DIGITS - secondFractionDigits);
- }
-
- // Profiling showed this method is important to log4j performance. Modify with care!
- // 28 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
- public String format(final long epochMillis) {
- final char[] result = new char[length << 1]; // double size for locales with lengthy DateFormatSymbols
- final int written = format(epochMillis, result, 0);
- return new String(result, 0, written);
- }
-
- // Profiling showed this method is important to log4j performance. Modify with care!
- // 31 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
- public int format(final long epochMillis, final char[] buffer, final int startPos) {
- // Calculate values by getting the ms values first and do then
- // calculate the hour minute and second values divisions.
-
- // Get daytime in ms: this does fit into an int
- // int ms = (int) (time % 86400000);
- final int ms = (int) (millisSinceMidnight(epochMillis));
- writeDate(buffer, startPos);
- final int pos = writeTime(ms, buffer, startPos + dateLength);
- return pos - startPos;
- }
-
- // Profiling showed this method is important to log4j performance. Modify with care!
- // 22 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
- private void writeDate(final char[] buffer, final int startPos) {
- if (cachedDate != null) {
- System.arraycopy(cachedDate, 0, buffer, startPos, dateLength);
- }
- }
-
- // Profiling showed this method is important to log4j performance. Modify with care!
- // 262 bytes (will be inlined when hot enough: <= -XX:FreqInlineSize=325 bytes on Linux)
- private int writeTime(int ms, final char[] buffer, int pos) {
- final int hourOfDay = ms / 3600000;
- final int hours = hourOfDay + daylightSavingTime(hourOfDay) / 3600000;
- ms -= 3600000 * hourOfDay;
-
- final int minutes = ms / 60000;
- ms -= 60000 * minutes;
-
- final int seconds = ms / 1000;
- ms -= 1000 * seconds;
-
- // Hour
- int temp = hours / 10;
- buffer[pos++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[pos++] = ((char) (hours - 10 * temp + '0'));
- buffer[pos] = timeSeparatorChar;
- pos += timeSeparatorLength;
-
- // Minute
- temp = minutes / 10;
- buffer[pos++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[pos++] = ((char) (minutes - 10 * temp + '0'));
- buffer[pos] = timeSeparatorChar;
- pos += timeSeparatorLength;
-
- // Second
- temp = seconds / 10;
- buffer[pos++] = ((char) (temp + '0'));
- buffer[pos++] = ((char) (seconds - 10 * temp + '0'));
- buffer[pos] = millisSeparatorChar;
- pos += millisSeparatorLength;
-
- // Millisecond
- temp = ms / 100;
- buffer[pos++] = ((char) (temp + '0'));
-
- ms -= 100 * temp;
- temp = ms / 10;
- buffer[pos++] = ((char) (temp + '0'));
-
- ms -= 10 * temp;
- buffer[pos++] = ((char) (ms + '0'));
- return pos;
- }
-
- private int writeTimeZone(final long epochMillis, final char[] buffer, int pos) {
- if (fixedTimeZoneFormat != null) {
- pos = fixedTimeZoneFormat.write(timeZone.getOffset(epochMillis), buffer, pos);
- }
- return pos;
- }
-
- static final int[] TABLE = {
- 100000, // 0
- 10000, // 1
- 1000, // 2
- 100, // 3
- 10, // 4
- 1, // 5
- };
-
- private int formatNanoOfMillisecond(final int nanoOfMillisecond, final char[] buffer, int pos) {
- int temp;
- int remain = nanoOfMillisecond;
- for (int i = 0; i < secondFractionDigits - FixedFormat.MILLI_FRACTION_DIGITS; i++) {
- final int divisor = TABLE[i];
- temp = remain / divisor;
- buffer[pos++] = ((char) (temp + '0'));
- remain -= divisor * temp; // equivalent of remain % 10
- }
- return pos;
- }
-
- private int daylightSavingTime(final int hourOfDay) {
- return hourOfDay > 23 ? dstOffsets[23] : dstOffsets[hourOfDay];
- }
-
- /**
- * Returns {@code true} if the old and new date values will result in the same formatted output, {@code false}
- * if results <i>may</i> differ.
- */
- public boolean isEquivalent(
- final long oldEpochSecond, final int oldNanoOfSecond, final long epochSecond, final int nanoOfSecond) {
- if (oldEpochSecond == epochSecond) {
- if (secondFractionDigits <= 3) {
- // Convert nanos to milliseconds for comparison if the format only requires milliseconds.
- return (oldNanoOfSecond / 1000_000L) == (nanoOfSecond / 1000_000L);
- }
- return oldNanoOfSecond == nanoOfSecond;
- }
- return false;
- }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/Format.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/Format.java
deleted file mode 100644
index dfecffbabe..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/Format.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time.internal.format;
-
-import java.text.FieldPosition;
-
-/**
- * The basic methods for performing date formatting.
- */
-public abstract class Format {
-
- public final String format(final Object obj) {
- return format(obj, new StringBuilder(), new FieldPosition(0)).toString();
- }
-
- public abstract StringBuilder format(Object obj, StringBuilder toAppendTo, FieldPosition pos);
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FormatCache.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FormatCache.java
deleted file mode 100644
index 303255c663..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FormatCache.java
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.time.internal.format;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-/**
- * <p>FormatCache is a cache and factory for {@link Format}s.</p>
- *
- * <p>
- * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
- * </p>
- *
- * @since Apache Commons Lang 3.0
- */
-// TODO: Before making public move from getDateTimeInstance(Integer,...) to int; or some other approach.
-abstract class FormatCache<F extends Format> {
-
- /**
- * No date or no time. Used in same parameters as DateFormat.SHORT or DateFormat.LONG
- */
- static final int NONE = -1;
-
- private final ConcurrentMap<MultipartKey, F> cInstanceCache = new ConcurrentHashMap<>(7);
-
- private static final ConcurrentMap<MultipartKey, String> cDateTimeInstanceCache = new ConcurrentHashMap<>(7);
-
- /**
- * <p>Gets a formatter instance using the default pattern in the
- * default timezone and locale.</p>
- *
- * @return a date/time formatter
- */
- public F getInstance() {
- return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault());
- }
-
- /**
- * <p>Gets a formatter instance using the specified pattern, time zone
- * and locale.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible
- * pattern, non-null
- * @param timeZone the time zone, null means use the default TimeZone
- * @param locale the locale, null means use the default Locale
- * @return a pattern based date/time formatter
- * @throws IllegalArgumentException if pattern is invalid
- * or <code>null</code>
- */
- public F getInstance(final String pattern, TimeZone timeZone, Locale locale) {
- if (pattern == null) {
- throw new NullPointerException("pattern must not be null");
- }
- if (timeZone == null) {
- timeZone = TimeZone.getDefault();
- }
- if (locale == null) {
- locale = Locale.getDefault();
- }
- final MultipartKey key = new MultipartKey(pattern, timeZone, locale);
- F format = cInstanceCache.get(key);
- if (format == null) {
- format = createInstance(pattern, timeZone, locale);
- final F previousValue = cInstanceCache.putIfAbsent(key, format);
- if (previousValue != null) {
- // another thread snuck in and did the same work
- // we should return the instance that is in ConcurrentMap
- format = previousValue;
- }
- }
- return format;
- }
-
- /**
- * <p>Create a format instance using the specified pattern, time zone
- * and locale.</p>
- *
- * @param pattern {@link java.text.SimpleDateFormat} compatible pattern, this will not be null.
- * @param timeZone time zone, this will not be null.
- * @param locale locale, this will not be null.
- * @return a pattern based date/time formatter
- * @throws IllegalArgumentException if pattern is invalid
- * or <code>null</code>
- */
- protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale);
-
- /**
- * <p>Gets a date/time formatter instance using the specified style,
- * time zone and locale.</p>
- *
- * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format
- * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format
- * @param timeZone optional time zone, overrides time zone of
- * formatted date, null means use default Locale
- * @param locale optional locale, overrides system locale
- * @return a localized standard date/time formatter
- * @throws IllegalArgumentException if the Locale has no date/time
- * pattern defined
- */
- // This must remain private, see LANG-884
- private F getDateTimeInstance(
- final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) {
- if (locale == null) {
- locale = Locale.getDefault();
- }
- final String pattern = getPatternForStyle(dateStyle, timeStyle, locale);
- return getInstance(pattern, timeZone, locale);
- }
-
- /**
- * <p>Gets a date/time formatter instance using the specified style,
- * time zone and locale.</p>
- *
- * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT
- * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted date, null means use default Locale
- * @param locale optional locale, overrides system locale
- * @return a localized standard date/time formatter
- * @throws IllegalArgumentException if the Locale has no date/time
- * pattern defined
- */
- // package protected, for access from FastDateFormat; do not make public or protected
- F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
- return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale);
- }
-
- /**
- * <p>Gets a date formatter instance using the specified style,
- * time zone and locale.</p>
- *
- * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted date, null means use default Locale
- * @param locale optional locale, overrides system locale
- * @return a localized standard date/time formatter
- * @throws IllegalArgumentException if the Locale has no date/time
- * pattern defined
- */
- // package protected, for access from FastDateFormat; do not make public or protected
- F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) {
- return getDateTimeInstance(Integer.valueOf(dateStyle), null, timeZone, locale);
- }
-
- /**
- * <p>Gets a time formatter instance using the specified style,
- * time zone and locale.</p>
- *
- * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT
- * @param timeZone optional time zone, overrides time zone of
- * formatted date, null means use default Locale
- * @param locale optional locale, overrides system locale
- * @return a localized standard date/time formatter
- * @throws IllegalArgumentException if the Locale has no date/time
- * pattern defined
- */
- // package protected, for access from FastDateFormat; do not make public or protected
- F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) {
- return getDateTimeInstance(null, Integer.valueOf(timeStyle), timeZone, locale);
- }
-
- /**
- * <p>Gets a date/time format for the specified styles and locale.</p>
- *
- * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format
- * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format
- * @param locale The non-null locale of the desired format
- * @return a localized standard date/time format
- * @throws IllegalArgumentException if the Locale has no date/time pattern defined
- */
- // package protected, for access from test code; do not make public or protected
- static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) {
- final MultipartKey key = new MultipartKey(dateStyle, timeStyle, locale);
-
- String pattern = cDateTimeInstanceCache.get(key);
- if (pattern == null) {
- try {
- final DateFormat formatter;
- if (dateStyle == null) {
- formatter = DateFormat.getTimeInstance(timeStyle.intValue(), locale);
- } else if (timeStyle == null) {
- formatter = DateFormat.getDateInstance(dateStyle.intValue(), locale);
- } else {
- formatter = DateFormat.getDateTimeInstance(dateStyle.intValue(), timeStyle.intValue(), locale);
- }
- pattern = ((SimpleDateFormat) formatter).toPattern();
- final String previous = cDateTimeInstanceCache.putIfAbsent(key, pattern);
- if (previous != null) {
- // even though it doesn't matter if another thread put the pattern
- // it's still good practice to return the String instance that is
- // actually in the ConcurrentMap
- pattern = previous;
- }
- } catch (final ClassCastException ex) {
- throw new IllegalArgumentException("No date time pattern for locale: " + locale);
- }
- }
- return pattern;
- }
-
- // ----------------------------------------------------------------------
- /**
- * <p>Helper class to hold multi-part Map keys</p>
- */
- private static class MultipartKey {
- private final Object[] keys;
- private int hashCode;
-
- /**
- * Constructs an instance of <code>MultipartKey</code> to hold the specified objects.
- * @param keys the set of objects that make up the key. Each key may be null.
- */
- public MultipartKey(final Object... keys) {
- this.keys = keys;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean equals(final Object obj) {
- // Eliminate the usual boilerplate because
- // this inner static class is only used in a generic ConcurrentHashMap
- // which will not compare against other Object types
- return Arrays.equals(keys, ((MultipartKey) obj).keys);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int hashCode() {
- if (hashCode == 0) {
- int rc = 0;
- for (final Object key : keys) {
- if (key != null) {
- rc = rc * 7 + key.hashCode();
- }
- }
- hashCode = rc;
- }
- return hashCode;
- }
- }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/package-info.java
deleted file mode 100644
index aec2664fa4..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-/**
- * Log4j 2 date formatting classes.
- */
-package org.apache.logging.log4j.core.time.internal.format;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DatePrinter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DatePrinter.java
deleted file mode 100644
index 7214e443a8..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DatePrinter.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.util.datetime;
-
-import java.text.FieldPosition;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * Use {@link org.apache.logging.log4j.core.time.internal.format.DatePrinter}.
- */
-@Deprecated
-public interface DatePrinter {
-
- /**
- * <p>Formats a millisecond {@code long} value.</p>
- *
- * @param millis the millisecond value to format
- * @return the formatted string
- * @since 2.1
- */
- String format(long millis);
-
- /**
- * <p>Formats a {@code Date} object using a {@code GregorianCalendar}.</p>
- *
- * @param date the date to format
- * @return the formatted string
- */
- String format(Date date);
-
- /**
- * <p>Formats a {@code Calendar} object.</p>
- * The TimeZone set on the Calendar is only used to adjust the time offset.
- * The TimeZone specified during the construction of the Parser will determine the TimeZone
- * used in the formatted string.
- *
- * @param calendar the calendar to format.
- * @return the formatted string
- */
- String format(Calendar calendar);
-
- /**
- * <p>Formats a millisecond {@code long} value into the
- * supplied {@code Appendable}.</p>
- *
- * @param millis the millisecond value to format
- * @param buf the buffer to format into
- * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
- * @return the specified string buffer
- * @since 3.5
- */
- <B extends Appendable> B format(long millis, B buf);
-
- /**
- * <p>Formats a {@code Date} object into the
- * supplied {@code Appendable} using a {@code GregorianCalendar}.</p>
- *
- * @param date the date to format
- * @param buf the buffer to format into
- * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
- * @return the specified string buffer
- * @since 3.5
- */
- <B extends Appendable> B format(Date date, B buf);
-
- /**
- * <p>Formats a {@code Calendar} object into the supplied {@code Appendable}.</p>
- * The TimeZone set on the Calendar is only used to adjust the time offset.
- * The TimeZone specified during the construction of the Parser will determine the TimeZone
- * used in the formatted string.
- *
- * @param calendar the calendar to format
- * @param buf the buffer to format into
- * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
- * @return the specified string buffer
- * @since 3.5
- */
- <B extends Appendable> B format(Calendar calendar, B buf);
-
- // Accessors
- // -----------------------------------------------------------------------
- /**
- * <p>Gets the pattern used by this printer.</p>
- *
- * @return the pattern, {@link java.text.SimpleDateFormat} compatible
- */
- String getPattern();
-
- /**
- * <p>Gets the time zone used by this printer.</p>
- *
- * <p>This zone is always used for {@code Date} printing. </p>
- *
- * @return the time zone
- */
- TimeZone getTimeZone();
-
- /**
- * <p>Gets the locale used by this printer.</p>
- *
- * @return the locale
- */
- Locale getLocale();
-
- /**
- * <p>Formats a {@code Date}, {@code Calendar} or
- * {@code Long} (milliseconds) object.</p>
- *
- * @param obj the object to format
- * @param toAppendTo the buffer to append to
- * @param pos the position - ignored
- * @return the buffer passed in
- * @see java.text.DateFormat#format(Object, StringBuffer, FieldPosition)
- */
- StringBuilder format(Object obj, StringBuilder toAppendTo, FieldPosition pos);
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
deleted file mode 100644
index b15fd704dd..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.util.datetime;
-
-import java.text.FieldPosition;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- *
- * @deprecated Use {@link org.apache.logging.log4j.core.time.internal.format.FastDateFormat}
- */
-public class FastDateFormat extends Format implements DatePrinter {
-
- private org.apache.logging.log4j.core.time.internal.format.FastDateFormat formatter = null;
-
- public static FastDateFormat getInstance() {
- return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance());
- }
-
- public static FastDateFormat getInstance(final String pattern) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(pattern));
- }
-
- public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(pattern, timeZone));
- }
-
- public static FastDateFormat getInstance(final String pattern, final Locale locale) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(pattern, locale));
- }
-
- public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
- return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(
- pattern, timeZone, locale));
- }
-
- public static FastDateFormat getDateInstance(final int style) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateInstance(style));
- }
-
- public static FastDateFormat getDateInstance(final int style, final Locale locale) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateInstance(style, locale));
- }
-
- public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateInstance(style, timeZone));
- }
-
- public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone, final Locale locale) {
- return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateInstance(
- style, timeZone, locale));
- }
-
- public static FastDateFormat getTimeInstance(final int style) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getTimeInstance(style));
- }
-
- public static FastDateFormat getTimeInstance(final int style, final Locale locale) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getTimeInstance(style, locale));
- }
-
- public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone) {
- return new FastDateFormat(
- org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getTimeInstance(style, timeZone));
- }
-
- public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone, final Locale locale) {
- return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getTimeInstance(
- style, timeZone, locale));
- }
-
- public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle) {
- return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
- dateStyle, timeStyle));
- }
-
- public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final Locale locale) {
- return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
- dateStyle, timeStyle, locale));
- }
-
- public static FastDateFormat getDateTimeInstance(
- final int dateStyle, final int timeStyle, final TimeZone timeZone) {
- return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
- dateStyle, timeStyle, timeZone));
- }
-
- public static FastDateFormat getDateTimeInstance(
- final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
- return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
- dateStyle, timeStyle, timeZone, locale));
- }
-
- private FastDateFormat(final org.apache.logging.log4j.core.time.internal.format.FastDateFormat formatter) {
- this.formatter = formatter;
- }
-
- protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) {
- formatter = org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(
- pattern, timeZone, locale);
- }
-
- protected FastDateFormat(
- final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
- formatter = org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
- pattern, timeZone, locale, centuryStart);
- }
-
- @Override
- public StringBuilder format(final Object obj, final StringBuilder toAppendTo, final FieldPosition pos) {
- return formatter.format(obj, toAppendTo, pos);
- }
-
- @Override
- public String format(final long millis) {
- return formatter.format(millis);
- }
-
- @Override
- public String format(final Date date) {
- return formatter.format(date);
- }
-
- @Override
- public String format(final Calendar calendar) {
- return formatter.format(calendar);
- }
-
- @Override
- public <B extends Appendable> B format(final long millis, final B buf) {
- return formatter.format(millis, buf);
- }
-
- @Override
- public <B extends Appendable> B format(final Date date, final B buf) {
- return formatter.format(date, buf);
- }
-
- @Override
- public <B extends Appendable> B format(final Calendar calendar, final B buf) {
- return formatter.format(calendar, buf);
- }
-
- @Override
- public String getPattern() {
- return formatter.getPattern();
- }
-
- @Override
- public TimeZone getTimeZone() {
- return formatter.getTimeZone();
- }
-
- @Override
- public Locale getLocale() {
- return formatter.getLocale();
- }
-
- public int getMaxLengthEstimate() {
- return formatter.getMaxLengthEstimate();
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (obj instanceof FastDateFormat == false) {
- return false;
- }
- final FastDateFormat other = (FastDateFormat) obj;
- // no need to check parser, as it has same invariants as printer
- return formatter.equals(other.formatter);
- }
-
- @Override
- public int hashCode() {
- return formatter.hashCode();
- }
-
- @Override
- public String toString() {
- return formatter.toString();
- }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantFormatter.java
similarity index 54%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantFormatter.java
index 38ea25e1f4..e53777813d 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantFormatter.java
@@ -14,19 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.core.util.datetime;
+package org.apache.logging.log4j.core.util.internal.instant;
-import java.text.FieldPosition;
+import static java.util.Objects.requireNonNull;
+
+import java.time.temporal.ChronoUnit;
+import org.apache.logging.log4j.core.time.Instant;
/**
- * Use {@link org.apache.logging.log4j.core.time.internal.format.Format}.
+ * Contract for formatting {@link Instant}s.
+ *
+ * @since 2.25.0
*/
-@Deprecated
-public abstract class Format {
+public interface InstantFormatter {
+
+ /**
+ * @return the time precision of the formatted output
+ */
+ ChronoUnit getPrecision();
- public final String format(final Object obj) {
- return format(obj, new StringBuilder(), new FieldPosition(0)).toString();
+ default String format(final Instant instant) {
+ requireNonNull(instant, "instant");
+ final StringBuilder buffer = new StringBuilder();
+ formatTo(buffer, instant);
+ return buffer.toString();
}
- public abstract StringBuilder format(Object obj, StringBuilder toAppendTo, FieldPosition pos);
+ void formatTo(StringBuilder buffer, Instant instant);
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatter.java
new file mode 100644
index 0000000000..0ca4a982b3
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatter.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.util.internal.instant;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.temporal.ChronoUnit;
+import java.util.function.BiConsumer;
+import org.apache.logging.log4j.core.time.Instant;
+
+/**
+ * Formats an {@link Instant} numerically; e.g., formats its epoch<sup>1</sup> seconds.
+ * <p>
+ * <sup>1</sup> Epoch is a fixed instant on {@code 1970-01-01Z}.
+ * </p>
+ * <h2>Internal usage only!</h2>
+ * <p>
+ * This class is intended only for internal Log4j usage.
+ * <b>Log4j users should not use this class!</b>
+ * This class is not subject to any backward compatibility concerns.
+ * </p>
+ *
+ * @since 2.25.0
+ */
+public enum InstantNumberFormatter implements InstantFormatter {
+
+ /**
+ * Formats nanoseconds since epoch; e.g., {@code 1581082727982123456}.
+ */
+ EPOCH_NANOS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long nanos = epochNanos(instant);
+ buffer.append(nanos);
+ }),
+
+ /**
+ * Formats milliseconds since epoch, including the nanosecond fraction; e.g., {@code 1581082727982.123456}.
+ * The nanosecond fraction will be skipped if it is zero.
+ */
+ EPOCH_MILLIS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long nanos = epochNanos(instant);
+ buffer.append(nanos);
+ buffer.insert(buffer.length() - 6, '.');
+ }),
+
+ /**
+ * Formats milliseconds since epoch, excluding the nanosecond fraction; e.g., {@code 1581082727982}.
+ */
+ EPOCH_MILLIS_ROUNDED(ChronoUnit.MILLIS, (instant, buffer) -> {
+ final long millis = instant.getEpochMillisecond();
+ buffer.append(millis);
+ }),
+
+ /**
+ * Formats the nanosecond fraction of milliseconds since epoch; e.g., {@code 123456}.
+ */
+ EPOCH_MILLIS_NANOS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long nanos = epochNanos(instant);
+ final long fraction = nanos % 1_000_000L;
+ buffer.append(fraction);
+ }),
+
+ /**
+ * Formats seconds since epoch, including the nanosecond fraction; e.g., {@code 1581082727.982123456}.
+ * The nanosecond fraction will be skipped if it is zero.
+ */
+ EPOCH_SECONDS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long nanos = epochNanos(instant);
+ buffer.append(nanos);
+ buffer.insert(buffer.length() - 9, '.');
+ }),
+
+ /**
+ * Formats seconds since epoch, excluding the nanosecond fraction; e.g., {@code 1581082727}.
+ * The nanosecond fraction will be skipped if it is zero.
+ */
+ EPOCH_SECONDS_ROUNDED(ChronoUnit.SECONDS, (instant, buffer) -> {
+ final long seconds = instant.getEpochSecond();
+ buffer.append(seconds);
+ }),
+
+ /**
+ * Formats the nanosecond fraction of seconds since epoch; e.g., {@code 982123456}.
+ */
+ EPOCH_SECONDS_NANOS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long secondsNanos = instant.getNanoOfSecond();
+ buffer.append(secondsNanos);
+ });
+
+ private static long epochNanos(final Instant instant) {
+ final long nanos = Math.multiplyExact(1_000_000_000L, instant.getEpochSecond());
+ return Math.addExact(nanos, instant.getNanoOfSecond());
+ }
+
+ private final ChronoUnit precision;
+
+ private final BiConsumer<Instant, StringBuilder> formatter;
+
+ InstantNumberFormatter(final ChronoUnit precision, final BiConsumer<Instant, StringBuilder> formatter) {
+ this.precision = precision;
+ this.formatter = formatter;
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return precision;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ requireNonNull(buffer, "buffer");
+ requireNonNull(instant, "instant");
+ formatter.accept(instant, buffer);
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
new file mode 100644
index 0000000000..9c93dd3406
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
@@ -0,0 +1,831 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.util.internal.instant;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * An {@link InstantPatternFormatter} that uses {@link DateTimeFormatter} under the hood.
+ * The pattern is analyzed and parts that require a precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} are precomputed, cached, and updated once every {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD}.
+ * The rest is computed dynamically.
+ * <p>
+ * For instance, given the pattern {@code yyyy-MM-dd'T'HH:mm:ss.SSSX}, the generated formatter will
+ * </p>
+ * <ol>
+ * <li>Sequence the pattern and assign a time precision to each part (e.g., {@code MM} is of month precision)</li>
+ * <li>Precompute and cache the output for parts that are of precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} (i.e., {@code yyyy-MM-dd'T'HH:mm:} and {@code X}) and cache it</li>
+ * <li>Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss.SSS})</li>
+ * </ol>
+ * <h2>Implementation note</h2>
+ * <p>
+ * Formatting can actually even be made faster and garbage-free by manually formatting sub-minute precision directives as follows:
+ * </p>
+ * <pre>{@code
+ * int offsetMillis = timeZone.getOffset(mutableInstant.getEpochMillisecond());
+ * long adjustedEpochSeconds = (instant.getEpochMillisecond() + offsetMillis) / 1000;
+ * int local_s = (int) (adjustedEpochSeconds % 60);
+ * int local_S = instant.getNanoOfSecond() / 100000000;
+ * int local_SS = instant.getNanoOfSecond() / 10000000;
+ * int local_SSS = instant.getNanoOfSecond() / 1000000;
+ * int local_SSSS = instant.getNanoOfSecond() / 100000;
+ * int local_SSSSS = instant.getNanoOfSecond() / 10000;
+ * int local_SSSSSS = instant.getNanoOfSecond() / 1000;
+ * int local_SSSSSSS = instant.getNanoOfSecond() / 100;
+ * int local_SSSSSSSS = instant.getNanoOfSecond() / 10;
+ * int local_SSSSSSSSS = instant.getNanoOfSecond();
+ * int local_n = instant.getNanoOfSecond();
+ * }</pre>
+ * <p>
+ * Though this will require more hardcoded formatting and a change in the sequence merging strategies.
+ * Hence, this optimization is intentionally shelved off due to involved complexity.
+ * See {@code verify_manually_computed_sub_minute_precision_values()} in {@code InstantPatternDynamicFormatterTest} for a demonstration of this optimization.
+ * </p>
+ *
+ * @since 2.25.0
+ */
+final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
+
+ static final ChronoUnit PRECISION_THRESHOLD = ChronoUnit.MINUTES;
+
+ private final AtomicReference<TimestampedFormatter> timestampedFormatterRef;
+
+ InstantPatternDynamicFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
+ final TimestampedFormatter timestampedFormatter = createTimestampedFormatter(pattern, locale, timeZone, null);
+ this.timestampedFormatterRef = new AtomicReference<>(timestampedFormatter);
+ }
+
+ @Override
+ public String getPattern() {
+ return timestampedFormatterRef.get().formatter.getPattern();
+ }
+
+ @Override
+ public Locale getLocale() {
+ return timestampedFormatterRef.get().formatter.getLocale();
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return timestampedFormatterRef.get().formatter.getTimeZone();
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return timestampedFormatterRef.get().formatter.getPrecision();
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ requireNonNull(buffer, "buffer");
+ requireNonNull(instant, "instant");
+ getEffectiveFormatter(instant).formatTo(buffer, instant);
+ }
+
+ private InstantPatternFormatter getEffectiveFormatter(final Instant instant) {
+
+ // Reuse the instance formatter, if timestamps match
+ TimestampedFormatter oldTimestampedFormatter = timestampedFormatterRef.get();
+ final long instantEpochMinutes = toEpochMinutes(instant);
+ final InstantPatternFormatter oldFormatter = oldTimestampedFormatter.formatter;
+ if (oldTimestampedFormatter.instantEpochMinutes == instantEpochMinutes) {
+ return oldFormatter;
+ }
+
+ // Create a new formatter, [try to] update the instance formatter, and return that
+ final TimestampedFormatter newTimestampedFormatter = createTimestampedFormatter(
+ oldFormatter.getPattern(), oldFormatter.getLocale(), oldFormatter.getTimeZone(), instant);
+ timestampedFormatterRef.compareAndSet(oldTimestampedFormatter, newTimestampedFormatter);
+ return newTimestampedFormatter.formatter;
+ }
+
+ private static TimestampedFormatter createTimestampedFormatter(
+ final String pattern, final Locale locale, final TimeZone timeZone, @Nullable Instant creationInstant) {
+ if (creationInstant == null) {
+ creationInstant = new MutableInstant();
+ final java.time.Instant currentInstant = java.time.Instant.now();
+ ((MutableInstant) creationInstant)
+ .initFromEpochSecond(currentInstant.getEpochSecond(), creationInstant.getNanoOfSecond());
+ }
+ final InstantPatternFormatter formatter =
+ createFormatter(pattern, locale, timeZone, PRECISION_THRESHOLD, creationInstant);
+ final long creationInstantEpochMinutes = toEpochMinutes(creationInstant);
+ return new TimestampedFormatter(creationInstantEpochMinutes, formatter);
+ }
+
+ private static final class TimestampedFormatter {
+
+ private final long instantEpochMinutes;
+
+ private final InstantPatternFormatter formatter;
+
+ private TimestampedFormatter(final long instantEpochMinutes, final InstantPatternFormatter formatter) {
+ this.instantEpochMinutes = instantEpochMinutes;
+ this.formatter = formatter;
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static InstantPatternFormatter createFormatter(
+ final String pattern,
+ final Locale locale,
+ final TimeZone timeZone,
+ final ChronoUnit precisionThreshold,
+ final Instant creationInstant) {
+
+ // Sequence the pattern and create associated formatters
+ final List<PatternSequence> sequences = sequencePattern(pattern, precisionThreshold);
+ final List<InstantPatternFormatter> formatters = sequences.stream()
+ .map(sequence -> {
+ final InstantPatternFormatter formatter = sequence.createFormatter(locale, timeZone);
+ final boolean constant = sequence.isConstantForDurationOf(precisionThreshold);
+ if (!constant) {
+ return formatter;
+ }
+ final String formattedInstant;
+ {
+ final StringBuilder buffer = new StringBuilder();
+ formatter.formatTo(buffer, creationInstant);
+ formattedInstant = buffer.toString();
+ }
+ return new AbstractFormatter(formatter.getPattern(), locale, timeZone, formatter.getPrecision()) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ buffer.append(formattedInstant);
+ }
+ };
+ })
+ .collect(Collectors.toList());
+
+ switch (formatters.size()) {
+
+ // If found an empty pattern, return an empty formatter
+ case 0:
+ return new AbstractFormatter(pattern, locale, timeZone, ChronoUnit.FOREVER) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ // Do nothing
+ }
+ };
+
+ // If extracted a single formatter, return it as is
+ case 1:
+ return formatters.get(0);
+
+ // Combine all extracted formatters into one
+ default:
+ final ChronoUnit precision = new CompositePatternSequence(sequences).precision;
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int formatterIndex = 0; formatterIndex < formatters.size(); formatterIndex++) {
+ final InstantPatternFormatter formatter = formatters.get(formatterIndex);
+ formatter.formatTo(buffer, instant);
+ }
+ }
+ };
+ }
+ }
+
+ static List<PatternSequence> sequencePattern(final String pattern, final ChronoUnit precisionThreshold) {
+ List<PatternSequence> sequences = sequencePattern(pattern);
+ final List<PatternSequence> mergedSequences = mergeDynamicSequences(sequences, precisionThreshold);
+ return mergeConsequentEffectivelyConstantSequences(mergedSequences, precisionThreshold);
+ }
+
+ private static List<PatternSequence> sequencePattern(final String pattern) {
+ if (pattern.isEmpty()) {
+ return Collections.emptyList();
+ }
+ final List<PatternSequence> sequences = new ArrayList<>();
+ for (int startIndex = 0; startIndex < pattern.length(); ) {
+ final char c = pattern.charAt(startIndex);
+
+ // Handle dynamic pattern letters
+ final boolean dynamic = isDynamicPatternLetter(c);
+ if (dynamic) {
+ int endIndex = startIndex + 1;
+ while (endIndex < pattern.length() && pattern.charAt(endIndex) == c) {
+ endIndex++;
+ }
+ final String sequenceContent = pattern.substring(startIndex, endIndex);
+ final PatternSequence sequence = new DynamicPatternSequence(sequenceContent);
+ sequences.add(sequence);
+ startIndex = endIndex;
+ }
+
+ // Handle single-quotes
+ else if (c == '\'') {
+ final int endIndex = pattern.indexOf('\'', startIndex + 1);
+ if (endIndex < 0) {
+ final String message = String.format(
+ "pattern ends with an incomplete string literal that started at index %d: `%s`",
+ startIndex, pattern);
+ throw new IllegalArgumentException(message);
+ }
+ final String sequenceLiteral =
+ (startIndex + 1) == endIndex ? "'" : pattern.substring(startIndex + 1, endIndex);
+ final PatternSequence sequence = new StaticPatternSequence(sequenceLiteral);
+ sequences.add(sequence);
+ startIndex = endIndex + 1;
+ }
+
+ // Handle unknown literal
+ else {
+ final PatternSequence sequence = new StaticPatternSequence("" + c);
+ sequences.add(sequence);
+ startIndex++;
+ }
+ }
+ return mergeConsequentStaticPatternSequences(sequences);
+ }
+
+ private static boolean isDynamicPatternLetter(final char c) {
+ return "GuyDMLdgQqYwWEecFaBhKkHmsSAnNVvzOXxZ".indexOf(c) >= 0;
+ }
+
+ /**
+ * Merges consequent static sequences.
+ *
+ * <p>
+ * For example, the sequencing of the {@code [MM-dd] HH:mm} pattern will create two static sequences for {@code ]} (right brace) and {@code } (whitespace) characters.
+ * This method will combine such consequent static sequences into one.
+ * </p>
+ *
+ * <h2>Example</h2>
+ *
+ * <p>
+ * The {@code [MM-dd] HH:mm} pattern will result in following sequences:
+ * </p>
+ *
+ * <pre>{@code
+ * [
+ * static(literal="["),
+ * dynamic(pattern="MM", precision=MONTHS),
+ * static(literal="-"),
+ * dynamic(pattern="dd", precision=DAYS),
+ * static(literal="]"),
+ * static(literal=" "),
+ * dynamic(pattern="HH", precision=HOURS),
+ * static(literal=":"),
+ * dynamic(pattern="mm", precision=MINUTES)
+ * ]
+ * }</pre>
+ *
+ * <p>
+ * The above sequencing implies creation of 9 {@link AbstractFormatter}s.
+ * This method transforms it to the following:
+ * </p>
+ *
+ * <pre>{@code
+ * [
+ * static(literal="["),
+ * dynamic(pattern="MM", precision=MONTHS),
+ * static(literal="-"),
+ * dynamic(pattern="dd", precision=DAYS),
+ * static(literal="] "),
+ * dynamic(pattern="HH", precision=HOURS),
+ * static(literal=":"),
+ * dynamic(pattern="mm", precision=MINUTES)
+ * ]
+ * }</pre>
+ *
+ * <p>
+ * The above sequencing implies creation of 8 {@link AbstractFormatter}s.
+ * </p>
+ *
+ * @param sequences sequences to be transformed
+ * @return transformed sequencing where consequent static sequences are merged
+ */
+ private static List<PatternSequence> mergeConsequentStaticPatternSequences(final List<PatternSequence> sequences) {
+
+ // Short-circuit if there is nothing to merge
+ if (sequences.size() < 2) {
+ return sequences;
+ }
+
+ final List<PatternSequence> mergedSequences = new ArrayList<>();
+ final List<StaticPatternSequence> accumulatedSequences = new ArrayList<>();
+ for (final PatternSequence sequence : sequences) {
+
+ // Spotted a static sequence? Stage it for merging.
+ if (sequence instanceof StaticPatternSequence) {
+ accumulatedSequences.add((StaticPatternSequence) sequence);
+ }
+
+ // Spotted a dynamic sequence.
+ // Merge the accumulated static sequences, and then append the dynamic sequence.
+ else {
+ mergeConsequentStaticPatternSequences(mergedSequences, accumulatedSequences);
+ mergedSequences.add(sequence);
+ }
+ }
+
+ // Merge leftover static sequences
+ mergeConsequentStaticPatternSequences(mergedSequences, accumulatedSequences);
+ return mergedSequences;
+ }
+
+ private static void mergeConsequentStaticPatternSequences(
+ final List<PatternSequence> mergedSequences, final List<StaticPatternSequence> accumulatedSequences) {
+ mergeAccumulatedSequences(mergedSequences, accumulatedSequences, () -> {
+ final String literal = accumulatedSequences.stream()
+ .map(sequence -> sequence.literal)
+ .collect(Collectors.joining());
+ return new StaticPatternSequence(literal);
+ });
+ }
+
+ /**
+ * Merges the sequences in between the first and the last found dynamic (i.e., non-constant) sequences.
+ *
+ * <p>
+ * For example, given the {@code ss.SSS} pattern – where {@code ss} and {@code SSS} is effectively not constant, yet {@code .} is – this method will combine it into a single dynamic sequence.
+ * Because, as demonstrated in {@code DateTimeFormatterSequencingBenchmark}, formatting {@code ss.SSS} is approximately 20% faster than formatting first {@code ss}, then manually appending a {@code .}, and then formatting {@code SSS}.
+ * </p>
+ *
+ * <h2>Example</h2>
+ *
+ * <p>
+ * Assume {@link #mergeConsequentStaticPatternSequences(List)} produced the following:
+ * </p>
+ *
+ * <pre>{@code
+ * [
+ * dynamic(pattern="yyyy", precision=YEARS),
+ * static(literal="-"),
+ * dynamic(pattern="MM", precision=MONTHS),
+ * static(literal="-"),
+ * dynamic(pattern="dd", precision=DAYS),
+ * static(literal="T"),
+ * dynamic(pattern="HH", precision=HOURS),
+ * static(literal=":"),
+ * dynamic(pattern="mm", precision=MINUTES),
+ * static(literal=":"),
+ * dynamic(pattern="ss", precision=SECONDS),
+ * static(literal="."),
+ * dynamic(pattern="SSS", precision=MILLISECONDS),
+ * dynamic(pattern="X", precision=HOURS),
+ * ]
+ * }</pre>
+ *
+ * <p>
+ * For a threshold precision of {@link ChronoUnit#MINUTES}, this sequencing effectively translates to two {@link DateTimeFormatter#formatTo(TemporalAccessor, Appendable)} invocations for each {@link #formatTo(StringBuilder, Instant)} call: one for {@code ss}, and another one for {@code SSS}.
+ * This method transforms the above sequencing into the following:
+ * </p>
+ *
+ * <pre>{@code
+ * [
+ * dynamic(pattern="yyyy", precision=YEARS),
+ * static(literal="-"),
+ * dynamic(pattern="MM", precision=MONTHS),
+ * static(literal="-"),
+ * dynamic(pattern="dd", precision=DAYS),
+ * static(literal="T"),
+ * dynamic(pattern="HH", precision=HOURS),
+ * static(literal=":"),
+ * dynamic(pattern="mm", precision=MINUTES),
+ * static(literal=":"),
+ * composite(
+ * sequences=[
+ * dynamic(pattern="ss", precision=SECONDS),
+ * static(literal="."),
+ * dynamic(pattern="SSS", precision=MILLISECONDS)
+ * ],
+ * precision=MILLISECONDS),
+ * dynamic(pattern="X", precision=HOURS),
+ * ]
+ * }</pre>
+ *
+ * <p>
+ * The resultant sequencing effectively translates to a single {@link DateTimeFormatter#formatTo(TemporalAccessor, Appendable)} invocation for each {@link #formatTo(StringBuilder, Instant)} call: only one fore {@code ss.SSS}.
+ * </p>
+ *
+ * @param sequences sequences, preferable produced by {@link #mergeConsequentStaticPatternSequences(List)}, to be transformed
+ * @param precisionThreshold a precision threshold to determine dynamic (i.e., non-constant) sequences
+ * @return transformed sequencing where sequences in between the first and the last found dynamic (i.e., non-constant) sequences are merged
+ */
+ private static List<PatternSequence> mergeDynamicSequences(
+ final List<PatternSequence> sequences, final ChronoUnit precisionThreshold) {
+
+ // Locate the first and the last dynamic (i.e., non-constant) sequence indices
+ int firstDynamicSequenceIndex = -1;
+ int lastDynamicSequenceIndex = -1;
+ for (int sequenceIndex = 0; sequenceIndex < sequences.size(); sequenceIndex++) {
+ final PatternSequence sequence = sequences.get(sequenceIndex);
+ final boolean constant = sequence.isConstantForDurationOf(precisionThreshold);
+ if (!constant) {
+ if (firstDynamicSequenceIndex < 0) {
+ firstDynamicSequenceIndex = sequenceIndex;
+ }
+ lastDynamicSequenceIndex = sequenceIndex;
+ }
+ }
+
+ // Short-circuit if there are less than 2 dynamic sequences
+ if (firstDynamicSequenceIndex < 0 || firstDynamicSequenceIndex == lastDynamicSequenceIndex) {
+ return sequences;
+ }
+
+ // Merge dynamic sequences
+ final List<PatternSequence> mergedSequences = new ArrayList<>();
+ if (firstDynamicSequenceIndex > 0) {
+ mergedSequences.addAll(sequences.subList(0, firstDynamicSequenceIndex));
+ }
+ final PatternSequence mergedDynamicSequence = new CompositePatternSequence(
+ sequences.subList(firstDynamicSequenceIndex, lastDynamicSequenceIndex + 1));
+ mergedSequences.add(mergedDynamicSequence);
+ if ((lastDynamicSequenceIndex + 1) < sequences.size()) {
+ mergedSequences.addAll(sequences.subList(lastDynamicSequenceIndex + 1, sequences.size()));
+ }
+ return mergedSequences;
+ }
+
+ /**
+ * Merges sequences that are consequent and effectively constant for the provided precision threshold.
+ *
+ * <p>
+ * For example, given the {@code yyyy-MM-dd'T'HH:mm:ss.SSS} pattern and a precision threshold of {@link ChronoUnit#MINUTES}, this method will combine sequences associated with {@code yyyy-MM-dd'T'HH:mm:} into a single sequence, since these are consequent and effectively constant sequences.
+ * </p>
+ *
+ * <h2>Example</h2>
+ *
+ * <p>
+ * Assume {@link #mergeDynamicSequences(List, ChronoUnit)} produced the following:
+ * </p>
+ *
+ * <pre>{@code
+ * [
+ * dynamic(pattern="yyyy", precision=YEARS),
+ * static(literal="-"),
+ * dynamic(pattern="MM", precision=MONTHS),
+ * static(literal="-"),
+ * dynamic(pattern="dd", precision=DAYS),
+ * static(literal="T"),
+ * dynamic(pattern="HH", precision=HOURS),
+ * static(literal=":"),
+ * dynamic(pattern="mm", precision=MINUTES),
+ * static(literal=":"),
+ * composite(
+ * sequences=[
+ * dynamic(pattern="ss", precision=SECONDS),
+ * static(literal="."),
+ * dynamic(pattern="SSS", precision=MILLISECONDS)
+ * ],
+ * precision=MILLISECONDS),
+ * dynamic(pattern="X", precision=HOURS),
+ * ]
+ * }</pre>
+ *
+ * <p>
+ * The above sequencing implies creation of 12 {@link AbstractFormatter}s.
+ * This method transforms it to the following:
+ * </p>
+ *
+ * <pre>{@code
+ * [
+ * composite(
+ * sequences=[
+ * dynamic(pattern="yyyy", precision=YEARS),
+ * static(literal="-"),
+ * dynamic(pattern="MM", precision=MONTHS),
+ * static(literal="-"),
+ * dynamic(pattern="dd", precision=DAYS),
+ * static(literal="T"),
+ * dynamic(pattern="HH", precision=HOURS),
+ * static(literal=":"),
+ * dynamic(pattern="mm", precision=MINUTES),
+ * static(literal=":")
+ * ],
+ * precision=MINUTES),
+ * composite(
+ * sequences=[
+ * dynamic(pattern="ss", precision=SECONDS),
+ * static(literal="."),
+ * dynamic(pattern="SSS", precision=MILLISECONDS)
+ * ],
+ * precision=MILLISECONDS),
+ * dynamic(pattern="X", precision=HOURS),
+ * ]
+ * }</pre>
+ *
+ * <p>
+ * The resultant sequencing effectively translates to 3 {@link AbstractFormatter}s.
+ * </p>
+ *
+ * @param sequences sequences, preferable produced by {@link #mergeDynamicSequences(List, ChronoUnit)}, to be transformed
+ * @param precisionThreshold a precision threshold to determine effectively constant sequences
+ * @return transformed sequencing where sequences that are consequent and effectively constant for the provided precision threshold are merged
+ */
+ private static List<PatternSequence> mergeConsequentEffectivelyConstantSequences(
+ final List<PatternSequence> sequences, final ChronoUnit precisionThreshold) {
+
+ // Short-circuit if there is nothing to merge
+ if (sequences.size() < 2) {
+ return sequences;
+ }
+
+ final List<PatternSequence> mergedSequences = new ArrayList<>();
+ boolean accumulatorConstant = true;
+ final List<PatternSequence> accumulatedSequences = new ArrayList<>();
+ for (final PatternSequence sequence : sequences) {
+ final boolean sequenceConstant = sequence.isConstantForDurationOf(precisionThreshold);
+ if (sequenceConstant != accumulatorConstant) {
+ mergeConsequentEffectivelyConstantSequences(mergedSequences, accumulatedSequences);
+ accumulatorConstant = sequenceConstant;
+ }
+ accumulatedSequences.add(sequence);
+ }
+
+ // Merge the accumulator leftover
+ mergeConsequentEffectivelyConstantSequences(mergedSequences, accumulatedSequences);
+ return mergedSequences;
+ }
+
+ private static void mergeConsequentEffectivelyConstantSequences(
+ final List<PatternSequence> mergedSequences, final List<PatternSequence> accumulatedSequences) {
+ mergeAccumulatedSequences(
+ mergedSequences, accumulatedSequences, () -> new CompositePatternSequence(accumulatedSequences));
+ }
+
+ private static <S extends PatternSequence> void mergeAccumulatedSequences(
+ final List<PatternSequence> mergedSequences,
+ final List<S> accumulatedSequences,
+ final Supplier<PatternSequence> mergedSequenceSupplier) {
+ if (accumulatedSequences.isEmpty()) {
+ return;
+ }
+ final PatternSequence mergedSequence =
+ accumulatedSequences.size() == 1 ? accumulatedSequences.get(0) : mergedSequenceSupplier.get();
+ mergedSequences.add(mergedSequence);
+ accumulatedSequences.clear();
+ }
+
+ private static long toEpochMinutes(final Instant instant) {
+ return instant.getEpochSecond() / 60;
+ }
+
+ private static TemporalAccessor toTemporalAccessor(final Instant instant) {
+ return instant instanceof TemporalAccessor
+ ? (TemporalAccessor) instant
+ : java.time.Instant.ofEpochSecond(instant.getEpochSecond(), instant.getNanoOfSecond());
+ }
+
+ private abstract static class AbstractFormatter implements InstantPatternFormatter {
+
+ private final String pattern;
+
+ private final Locale locale;
+
+ private final TimeZone timeZone;
+
+ private final ChronoUnit precision;
+
+ private AbstractFormatter(
+ final String pattern, final Locale locale, final TimeZone timeZone, final ChronoUnit precision) {
+ this.pattern = pattern;
+ this.locale = locale;
+ this.timeZone = timeZone;
+ this.precision = precision;
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return precision;
+ }
+
+ @Override
+ public String getPattern() {
+ return pattern;
+ }
+
+ @Override
+ public Locale getLocale() {
+ return locale;
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+ }
+
+ abstract static class PatternSequence {
+
+ final String pattern;
+
+ final ChronoUnit precision;
+
+ @SuppressWarnings("ReturnValueIgnored")
+ PatternSequence(final String pattern, final ChronoUnit precision) {
+ DateTimeFormatter.ofPattern(pattern); // Validate the pattern
+ this.pattern = pattern;
+ this.precision = precision;
+ }
+
+ InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone) {
+ final DateTimeFormatter dateTimeFormatter =
+ DateTimeFormatter.ofPattern(pattern, locale).withZone(timeZone.toZoneId());
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ final TemporalAccessor instantAccessor = toTemporalAccessor(instant);
+ dateTimeFormatter.formatTo(instantAccessor, buffer);
+ }
+ };
+ }
+
+ private boolean isConstantForDurationOf(final ChronoUnit thresholdPrecision) {
+ return precision.compareTo(thresholdPrecision) >= 0;
+ }
+
+ @Override
+ public boolean equals(final Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ PatternSequence sequence = (PatternSequence) object;
+ return Objects.equals(pattern, sequence.pattern) && precision == sequence.precision;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pattern, precision);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("<%s>%s", pattern, precision);
+ }
+ }
+
+ static final class StaticPatternSequence extends PatternSequence {
+
+ private final String literal;
+
+ StaticPatternSequence(final String literal) {
+ super(literal.equals("'") ? "''" : ("'" + literal + "'"), ChronoUnit.FOREVER);
+ this.literal = literal;
+ }
+
+ @Override
+ InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone) {
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ buffer.append(literal);
+ }
+ };
+ }
+ }
+
+ static final class DynamicPatternSequence extends PatternSequence {
+
+ DynamicPatternSequence(final String content) {
+ super(content, contentPrecision(content));
+ }
+
+ /**
+ * @param content a single-letter directive content complying (e.g., {@code H}, {@code HH}, or {@code pHH})
+ * @return the time precision of the directive
+ */
+ @Nullable
+ private static ChronoUnit contentPrecision(final String content) {
+
+ validateContent(content);
+ final String paddingRemovedContent = removePadding(content);
+
+ if (paddingRemovedContent.matches("[GuyY]+")) {
+ return ChronoUnit.YEARS;
+ } else if (paddingRemovedContent.matches("[MLQq]+")) {
+ return ChronoUnit.MONTHS;
+ } else if (paddingRemovedContent.matches("[wW]+")) {
+ return ChronoUnit.WEEKS;
+ } else if (paddingRemovedContent.matches("[DdgEecF]+")) {
+ return ChronoUnit.DAYS;
+ } else if (paddingRemovedContent.matches("[aBhKkH]+")
+ // Time-zone directives
+ || paddingRemovedContent.matches("[ZxXOzvV]+")) {
+ return ChronoUnit.HOURS;
+ } else if (paddingRemovedContent.contains("m")) {
+ return ChronoUnit.MINUTES;
+ } else if (paddingRemovedContent.contains("s")) {
+ return ChronoUnit.SECONDS;
+ }
+
+ // 2 to 3 consequent `S` characters output millisecond precision
+ else if (paddingRemovedContent.matches("S{2,3}")
+ // `A` (milli-of-day) outputs millisecond precision.
+ || paddingRemovedContent.contains("A")) {
+ return ChronoUnit.MILLIS;
+ }
+
+ // 4 to 6 consequent `S` characters output microsecond precision
+ else if (paddingRemovedContent.matches("S{4,6}")) {
+ return ChronoUnit.MICROS;
+ }
+
+ // A single `S` (fraction-of-second) outputs nanosecond precision
+ else if (paddingRemovedContent.equals("S")
+ // 7 to 9 consequent `S` characters output nanosecond precision
+ || paddingRemovedContent.matches("S{7,9}")
+ // `n` (nano-of-second) and `N` (nano-of-day) always output nanosecond precision.
+ // This is independent of how many times they occur sequentially.
+ || paddingRemovedContent.matches("[nN]+")) {
+ return ChronoUnit.NANOS;
+ }
+
+ final String message = String.format("unrecognized pattern: `%s`", content);
+ throw new IllegalArgumentException(message);
+ }
+
+ private static void validateContent(final String content) {
+
+ // Is the content empty?
+ final String paddingRemovedContent = removePadding(content);
+ if (paddingRemovedContent.isEmpty()) {
+ final String message = String.format("empty content: `%s`", content);
+ throw new IllegalArgumentException(message);
+ }
+
+ // Does the content start with a recognized letter?
+ final char letter = paddingRemovedContent.charAt(0);
+ final boolean dynamic = isDynamicPatternLetter(letter);
+ if (!dynamic) {
+ String message =
+ String.format("pattern sequence doesn't start with a dynamic pattern letter: `%s`", content);
+ throw new IllegalArgumentException(message);
+ }
+
+ // Is the content composed of repetitions of the first letter?
+ final boolean repeated = paddingRemovedContent.matches("^(\\Q" + letter + "\\E)+$");
+ if (!repeated) {
+ String message = String.format(
+ "was expecting letter `%c` to be repeated through the entire pattern sequence: `%s`",
+ letter, content);
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ private static String removePadding(final String content) {
+ return content.replaceAll("^p+", "");
+ }
+ }
+
+ static final class CompositePatternSequence extends PatternSequence {
+
+ CompositePatternSequence(final List<PatternSequence> sequences) {
+ super(concatSequencePatterns(sequences), findSequenceMaxPrecision(sequences));
+ // Only allow two or more sequences
+ if (sequences.size() < 2) {
+ throw new IllegalArgumentException("was expecting two or more sequences: " + sequences);
+ }
+ }
+
+ @SuppressWarnings("OptionalGetWithoutIsPresent")
+ private static ChronoUnit findSequenceMaxPrecision(List<PatternSequence> sequences) {
+ return sequences.stream()
+ .map(sequence -> sequence.precision)
+ .min(Comparator.comparing(ChronoUnit::getDuration))
+ .get();
+ }
+
+ private static String concatSequencePatterns(List<PatternSequence> sequences) {
+ return sequences.stream().map(sequence -> sequence.pattern).collect(Collectors.joining());
+ }
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
new file mode 100644
index 0000000000..037e79a009
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.util.internal.instant;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.logging.log4j.util.Strings.isBlank;
+
+import java.time.temporal.ChronoUnit;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.util.Constants;
+
+/**
+ * Contract for formatting {@link Instant}s using a date and time formatting pattern.
+ * <h2>Internal usage only!</h2>
+ * <p>
+ * This class is intended only for internal Log4j usage.
+ * <b>Log4j users should not use this class!</b>
+ * This class is not subject to any backward compatibility concerns.
+ * </p>
+ *
+ * @since 2.25.0
+ */
+public interface InstantPatternFormatter extends InstantFormatter {
+
+ String getPattern();
+
+ Locale getLocale();
+
+ TimeZone getTimeZone();
+
+ static Builder newBuilder() {
+ return new Builder();
+ }
+
+ final class Builder {
+
+ private String pattern;
+
+ private Locale locale = Locale.getDefault();
+
+ private TimeZone timeZone = TimeZone.getDefault();
+
+ private boolean cachingEnabled = Constants.ENABLE_THREADLOCALS;
+
+ private Builder() {}
+
+ public String getPattern() {
+ return pattern;
+ }
+
+ public Builder setPattern(final String pattern) {
+ this.pattern = pattern;
+ return this;
+ }
+
+ public Locale getLocale() {
+ return locale;
+ }
+
+ public Builder setLocale(final Locale locale) {
+ this.locale = locale;
+ return this;
+ }
+
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ public Builder setTimeZone(final TimeZone timeZone) {
+ this.timeZone = timeZone;
+ return this;
+ }
+
+ public boolean isCachingEnabled() {
+ return cachingEnabled;
+ }
+
+ public Builder setCachingEnabled(boolean cachingEnabled) {
+ this.cachingEnabled = cachingEnabled;
+ return this;
+ }
+
+ public InstantPatternFormatter build() {
+
+ // Validate arguments
+ requireNonNull(locale, "locale");
+ requireNonNull(timeZone, "timeZone");
+
+ // Return a literal formatter if the pattern is blank
+ if (isBlank(pattern)) {
+ return createLiteralFormatter(pattern, locale, timeZone);
+ }
+
+ // Create the formatter, and return it, if caching is disabled
+ final InstantPatternDynamicFormatter formatter =
+ new InstantPatternDynamicFormatter(pattern, locale, timeZone);
+ if (!cachingEnabled) {
+ return formatter;
+ }
+
+ // Wrap the formatter with caching, if necessary
+ switch (formatter.getPrecision()) {
+
+ // It is not worth caching when a precision equal to or higher than microsecond is requested
+ case NANOS:
+ case MICROS:
+ return formatter;
+
+ // Millisecond precision cache
+ case MILLIS:
+ return InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(formatter);
+
+ // Cache everything else with second precision
+ default:
+ return InstantPatternThreadLocalCachedFormatter.ofSecondPrecision(formatter);
+ }
+ }
+
+ private static InstantPatternFormatter createLiteralFormatter(
+ final String literal, final Locale locale, final TimeZone timeZone) {
+ return new InstantPatternFormatter() {
+
+ @Override
+ public String getPattern() {
+ return literal;
+ }
+
+ @Override
+ public Locale getLocale() {
+ return locale;
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return ChronoUnit.FOREVER;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ buffer.append(literal);
+ }
+ };
+ }
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatter.java
new file mode 100644
index 0000000000..96bf4504aa
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatter.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.util.internal.instant;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.temporal.ChronoUnit;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.function.Function;
+import org.apache.logging.log4j.core.time.Instant;
+
+/**
+ * An {@link InstantFormatter} wrapper caching the last formatted output in a {@link ThreadLocal} and trying to reuse it.
+ *
+ * @since 2.25.0
+ */
+final class InstantPatternThreadLocalCachedFormatter implements InstantPatternFormatter {
+
+ private final InstantPatternFormatter formatter;
+
+ private final Function<Instant, Long> epochInstantExtractor;
+
+ private final ThreadLocal<Object[]> epochInstantAndBufferRef =
+ ThreadLocal.withInitial(InstantPatternThreadLocalCachedFormatter::createEpochInstantAndBuffer);
+
+ private Object[] lastEpochInstantAndBuffer = createEpochInstantAndBuffer();
+
+ private static Object[] createEpochInstantAndBuffer() {
+ return new Object[] {-1L, new StringBuilder()};
+ }
+
+ private final ChronoUnit precision;
+
+ private InstantPatternThreadLocalCachedFormatter(
+ final InstantPatternFormatter formatter,
+ final Function<Instant, Long> epochInstantExtractor,
+ final ChronoUnit precision) {
+ this.formatter = formatter;
+ this.epochInstantExtractor = epochInstantExtractor;
+ this.precision = precision;
+ }
+
+ static InstantPatternThreadLocalCachedFormatter ofMilliPrecision(final InstantPatternFormatter formatter) {
+ final ChronoUnit precision = effectivePrecision(formatter, ChronoUnit.MILLIS);
+ return new InstantPatternThreadLocalCachedFormatter(formatter, Instant::getEpochMillisecond, precision);
+ }
+
+ static InstantPatternThreadLocalCachedFormatter ofSecondPrecision(final InstantPatternFormatter formatter) {
+ final ChronoUnit precision = effectivePrecision(formatter, ChronoUnit.SECONDS);
+ return new InstantPatternThreadLocalCachedFormatter(formatter, Instant::getEpochSecond, precision);
+ }
+
+ private static ChronoUnit effectivePrecision(final InstantFormatter formatter, final ChronoUnit cachePrecision) {
+ final ChronoUnit formatterPrecision = formatter.getPrecision();
+ final int comparison = cachePrecision.compareTo(formatterPrecision);
+ if (comparison == 0) {
+ return formatterPrecision;
+ } else if (comparison > 0) {
+ final String message = String.format(
+ "instant formatter `%s` is of `%s` precision, whereas the requested cache precision is `%s`",
+ formatter, formatterPrecision, cachePrecision);
+ throw new IllegalArgumentException(message);
+ } else {
+ return cachePrecision;
+ }
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return precision;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ requireNonNull(buffer, "buffer");
+ requireNonNull(instant, "instant");
+ final Object[] prevEpochInstantAndBuffer = lastEpochInstantAndBuffer;
+ final long prevEpochInstant = (long) prevEpochInstantAndBuffer[0];
+ final StringBuilder prevBuffer = (StringBuilder) prevEpochInstantAndBuffer[1];
+ final long nextEpochInstant = epochInstantExtractor.apply(instant);
+ if (prevEpochInstant == nextEpochInstant) {
+ buffer.append(prevBuffer);
+ } else {
+
+ // We could have used `StringBuilders.trimToMaxSize()` on `prevBuffer`.
+ // That is, we wouldn't want exploded `StringBuilder`s in hundreds of `ThreadLocal`s.
+ // Though we are formatting instants and always expect to produce strings of more or less the same length.
+ // Hence, no need for truncation.
+
+ // Populate a new cache entry
+ final Object[] nextEpochInstantAndBuffer = epochInstantAndBufferRef.get();
+ nextEpochInstantAndBuffer[0] = nextEpochInstant;
+ final StringBuilder nextBuffer = (StringBuilder) nextEpochInstantAndBuffer[1];
+ nextBuffer.setLength(0);
+ formatter.formatTo(nextBuffer, instant);
+
+ // Update the effective cache entry
+ lastEpochInstantAndBuffer = nextEpochInstantAndBuffer;
+
+ // Help out the request
+ buffer.append(nextBuffer);
+ }
+ }
+
+ @Override
+ public String getPattern() {
+ return formatter.getPattern();
+ }
+
+ @Override
+ public Locale getLocale() {
+ return formatter.getLocale();
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return formatter.getTimeZone();
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
similarity index 61%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
index f801bb4b4e..3c87d12589 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
@@ -15,11 +15,23 @@
* limitations under the license.
*/
/**
- * Log4j 2 date formatting classes.
+ * Utilities for formatting log event {@link org.apache.logging.log4j.core.time.Instant}s.
+ * <h2>Internal usage only!</h2>
+ * <p>
+ * This package is intended only for internal Log4j usage.
+ * <b>Log4j users should not use this package!</b>
+ * This package is not subject to any backward compatibility concerns.
+ * </p>
+ *
+ * @since 2.25.0
*/
@Export
-@Version("3.0.0")
-package org.apache.logging.log4j.core.util.datetime;
+@ExportTo("org.apache.logging.log4j.layout.template.json")
+@Version("2.25.0")
+@NullMarked
+package org.apache.logging.log4j.core.util.internal.instant;
+import aQute.bnd.annotation.jpms.ExportTo;
+import org.jspecify.annotations.NullMarked;
import org.osgi.annotation.bundle.Export;
import org.osgi.annotation.versioning.Version;
diff --git a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/LogEventFixture.java b/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/LogEventFixture.java
index 21daa06819..bc9a36094d 100644
--- a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/LogEventFixture.java
+++ b/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/LogEventFixture.java
@@ -29,18 +29,24 @@ import org.apache.logging.log4j.spi.MutableThreadContextStack;
import org.apache.logging.log4j.spi.ThreadContextStack;
import org.apache.logging.log4j.util.StringMap;
-final class LogEventFixture {
+public final class LogEventFixture {
private LogEventFixture() {}
private static final int TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT = 10;
- static List<LogEvent> createLiteLogEvents(final int logEventCount) {
+ public static List<LogEvent> createLiteLogEvents(final int logEventCount) {
+ return createLiteLogEvents(logEventCount, TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT);
+ }
+
+ public static List<LogEvent> createLiteLogEvents(
+ final int logEventCount, final int timeOverlappingConsecutiveEventCount) {
final List<LogEvent> logEvents = new ArrayList<>(logEventCount);
final long startTimeMillis = System.currentTimeMillis();
for (int logEventIndex = 0; logEventIndex < logEventCount; logEventIndex++) {
final String logEventId = String.valueOf(logEventIndex);
- final long logEventTimeMillis = createLogEventTimeMillis(startTimeMillis, logEventIndex);
+ final long logEventTimeMillis =
+ createLogEventTimeMillis(startTimeMillis, logEventIndex, timeOverlappingConsecutiveEventCount);
final LogEvent logEvent = LogEventFixture.createLiteLogEvent(logEventId, logEventTimeMillis);
logEvents.add(logEvent);
}
@@ -63,24 +69,31 @@ final class LogEventFixture {
.build();
}
- static List<LogEvent> createFullLogEvents(final int logEventCount) {
+ public static List<LogEvent> createFullLogEvents(final int logEventCount) {
+ return createFullLogEvents(logEventCount, TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT);
+ }
+
+ public static List<LogEvent> createFullLogEvents(
+ final int logEventCount, final int timeOverlappingConsecutiveEventCount) {
final List<LogEvent> logEvents = new ArrayList<>(logEventCount);
final long startTimeMillis = System.currentTimeMillis();
for (int logEventIndex = 0; logEventIndex < logEventCount; logEventIndex++) {
final String logEventId = String.valueOf(logEventIndex);
- final long logEventTimeMillis = createLogEventTimeMillis(startTimeMillis, logEventIndex);
+ final long logEventTimeMillis =
+ createLogEventTimeMillis(startTimeMillis, logEventIndex, timeOverlappingConsecutiveEventCount);
final LogEvent logEvent = LogEventFixture.createFullLogEvent(logEventId, logEventTimeMillis);
logEvents.add(logEvent);
}
return logEvents;
}
- private static long createLogEventTimeMillis(final long startTimeMillis, final int logEventIndex) {
+ private static long createLogEventTimeMillis(
+ final long startTimeMillis, final int logEventIndex, final int timeOverlappingConsecutiveEventCount) {
// Create event time repeating every certain number of consecutive
// events. This is better aligned with the real-world use case and
// gives surface to timestamp formatter caches to perform their
// magic, which is implemented for almost all layouts.
- return startTimeMillis + logEventIndex / TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT;
+ return startTimeMillis + logEventIndex / timeOverlappingConsecutiveEventCount;
}
private static LogEvent createFullLogEvent(final String id, final long timeMillis) {
diff --git a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java b/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java
deleted file mode 100644
index d4d9924283..0000000000
--- a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@Export
-@Version("2.20.1")
-package org.apache.logging.log4j.layout.template.json;
-
-import org.osgi.annotation.bundle.Export;
-import org.osgi.annotation.versioning.Version;
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
index a9d0870e2f..f6bc105057 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
@@ -20,8 +20,9 @@ import java.util.Locale;
import java.util.TimeZone;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.time.Instant;
-import org.apache.logging.log4j.core.time.InstantFormatter;
-import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.internal.instant.InstantFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantNumberFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutProperties;
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
@@ -53,15 +54,14 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
* rounded = "rounded" -> boolean
* </pre>
*
- * If no configuration options are provided, <tt>pattern-config</tt> is
- * employed. There {@link
- * JsonTemplateLayoutProperties#timestampFormatPattern()}, {@link
- * JsonTemplateLayoutProperties#timeZone()}, {@link
- * JsonTemplateLayoutProperties#locale()} are used as defaults for
- * <tt>pattern</tt>, <tt>timeZone</tt>, and <tt>locale</tt>, respectively.
+ * <p>
+ * If no configuration options are provided, <tt>pattern-config</tt> is employed.
+ * There {@link JsonTemplateLayoutProperties#timestampFormatPattern()}, {@link JsonTemplateLayoutProperties#timeZone()}, {@link JsonTemplateLayoutProperties#locale()} are used as defaults for <tt>pattern</tt>, <tt>timeZone</tt>, and <tt>locale</tt>, respectively.
+ * </p>
*
- * In <tt>epoch-config</tt>, <tt>millis.nanos</tt>, <tt>secs.nanos</tt> stand
- * for the fractional component in nanoseconds.
+ * <p>
+ * In <tt>epoch-config</tt>, <tt>millis.nanos</tt>, <tt>secs.nanos</tt> stand for the fractional component in nanoseconds.
+ * </p>
*
* <h3>Examples</h3>
*
@@ -207,100 +207,55 @@ public final class TimestampResolver implements EventResolver {
return epochProvided ? createEpochResolver(config) : createPatternResolver(config);
}
- private static final class PatternResolverContext {
-
- private final InstantFormatter formatter;
-
- private final StringBuilder lastFormattedInstantBuffer = new StringBuilder();
-
- private final MutableInstant lastFormattedInstant = new MutableInstant();
-
- private PatternResolverContext(final String pattern, final TimeZone timeZone, final Locale locale) {
- this.formatter = InstantFormatter.newBuilder()
- .setPattern(pattern)
- .setTimeZone(timeZone)
- .setLocale(locale)
- .build();
- lastFormattedInstant.initFromEpochSecond(-1, 0);
- }
+ private static EventResolver createPatternResolver(final TemplateResolverConfig config) {
+ final String pattern = readPattern(config);
+ final TimeZone timeZone = readTimeZone(config);
+ final Locale locale = config.getLocale(new String[] {"pattern", "locale"});
+ final InstantFormatter formatter = InstantPatternFormatter.newBuilder()
+ .setPattern(pattern)
+ .setTimeZone(timeZone)
+ .setLocale(locale)
+ .build();
+ return new PatternResolver(formatter);
+ }
- private static PatternResolverContext fromConfig(final TemplateResolverConfig config) {
- final String pattern = readPattern(config);
- final TimeZone timeZone = readTimeZone(config);
- final Locale locale = config.getLocale(new String[] {"pattern", "locale"});
- return new PatternResolverContext(pattern, timeZone, locale);
- }
+ private static String readPattern(final TemplateResolverConfig config) {
+ final String format = config.getString(new String[] {"pattern", "format"});
+ return format != null ? format : config.getDefaults().timestampFormatPattern();
+ }
- private static String readPattern(final TemplateResolverConfig config) {
- final String format = config.getString(new String[] {"pattern", "format"});
- return format != null ? format : config.getDefaults().timestampFormatPattern();
+ private static TimeZone readTimeZone(final TemplateResolverConfig config) {
+ final String timeZoneId = config.getString(new String[] {"pattern", "timeZone"});
+ if (timeZoneId == null) {
+ return config.getDefaults().timeZone();
}
-
- private static TimeZone readTimeZone(final TemplateResolverConfig config) {
- final String timeZoneId = config.getString(new String[] {"pattern", "timeZone"});
- if (timeZoneId == null) {
- return config.getDefaults().timeZone();
- }
- boolean found = false;
- for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
- if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
- found = true;
- break;
- }
+ boolean found = false;
+ for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
+ if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
+ found = true;
+ break;
}
- if (!found) {
- throw new IllegalArgumentException("invalid timestamp time zone: " + config);
- }
- return TimeZone.getTimeZone(timeZoneId);
}
+ if (!found) {
+ throw new IllegalArgumentException("invalid timestamp time zone: " + config);
+ }
+ return TimeZone.getTimeZone(timeZoneId);
}
private static final class PatternResolver implements EventResolver {
- private final PatternResolverContext patternResolverContext;
+ private final InstantFormatter formatter;
- private PatternResolver(final PatternResolverContext patternResolverContext) {
- this.patternResolverContext = patternResolverContext;
+ private PatternResolver(final InstantFormatter formatter) {
+ this.formatter = formatter;
}
@Override
- public synchronized void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
-
- // Format timestamp if it doesn't match the last cached one.
- final boolean instantMatching = patternResolverContext.formatter.isInstantMatching(
- patternResolverContext.lastFormattedInstant, logEvent.getInstant());
- if (!instantMatching) {
-
- // Format the timestamp.
- patternResolverContext.lastFormattedInstantBuffer.setLength(0);
- patternResolverContext.lastFormattedInstant.initFrom(logEvent.getInstant());
- patternResolverContext.formatter.format(
- patternResolverContext.lastFormattedInstant, patternResolverContext.lastFormattedInstantBuffer);
-
- // Write the formatted timestamp.
- final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
- final int startIndex = jsonWriterStringBuilder.length();
- jsonWriter.writeString(patternResolverContext.lastFormattedInstantBuffer);
-
- // Cache the written value.
- patternResolverContext.lastFormattedInstantBuffer.setLength(0);
- patternResolverContext.lastFormattedInstantBuffer.append(
- jsonWriterStringBuilder, startIndex, jsonWriterStringBuilder.length());
-
- }
-
- // Write the cached formatted timestamp.
- else {
- jsonWriter.writeRawString(patternResolverContext.lastFormattedInstantBuffer);
- }
+ public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
+ jsonWriter.writeString(formatter::formatTo, logEvent.getInstant());
}
}
- private static EventResolver createPatternResolver(final TemplateResolverConfig config) {
- final PatternResolverContext patternResolverContext = PatternResolverContext.fromConfig(config);
- return new PatternResolver(patternResolverContext);
- }
-
private static EventResolver createEpochResolver(final TemplateResolverConfig config) {
final String unit = config.getString(new String[] {"epoch", "unit"});
final Boolean rounded = config.getBoolean(new String[] {"epoch", "rounded"});
@@ -318,108 +273,48 @@ public final class TimestampResolver implements EventResolver {
throw new IllegalArgumentException("invalid epoch configuration: " + config);
}
- private static final class EpochResolutionRecord {
-
- private static final int MAX_LONG_LENGTH =
- String.valueOf(Long.MAX_VALUE).length();
-
- private final MutableInstant instant = new MutableInstant();
-
- private final char[] resolution =
- new char[ /* integral: */MAX_LONG_LENGTH + /* dot: */ 1 + /* fractional: */ MAX_LONG_LENGTH];
-
- private int resolutionLength;
-
- private EpochResolutionRecord() {
- instant.initFromEpochSecond(-1, 0);
- }
- }
-
- private abstract static class EpochResolver implements EventResolver {
-
- private final EpochResolutionRecord resolutionRecord = new EpochResolutionRecord();
-
- @Override
- public synchronized void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
- final Instant logEventInstant = logEvent.getInstant();
- if (logEventInstant.equals(resolutionRecord.instant)) {
- jsonWriter.writeRawString(resolutionRecord.resolution, 0, resolutionRecord.resolutionLength);
- } else {
- resolutionRecord.instant.initFrom(logEventInstant);
- final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
- final int startIndex = stringBuilder.length();
- resolve(logEventInstant, jsonWriter);
- resolutionRecord.resolutionLength = stringBuilder.length() - startIndex;
- stringBuilder.getChars(startIndex, stringBuilder.length(), resolutionRecord.resolution, 0);
- }
- }
-
- abstract void resolve(Instant logEventInstant, JsonWriter jsonWriter);
- }
-
- private static final EventResolver EPOCH_NANOS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final long nanos = epochNanos(logEventInstant);
- jsonWriter.writeNumber(nanos);
- }
+ private static final EventResolver EPOCH_NANOS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_NANOS.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_MILLIS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
- final long nanos = epochNanos(logEventInstant);
- jsonWriterStringBuilder.append(nanos);
- jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 6, '.');
- }
+ private static final EventResolver EPOCH_MILLIS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_MILLIS.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_MILLIS_ROUNDED_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
- }
+ private static final EventResolver EPOCH_MILLIS_ROUNDED_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_MILLIS_ROUNDED.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_MILLIS_NANOS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final long nanos = epochNanos(logEventInstant);
- final long fraction = nanos % 1_000_000L;
- jsonWriter.writeNumber(fraction);
- }
+ private static final EventResolver EPOCH_MILLIS_NANOS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_MILLIS_NANOS.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_SECS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
- final long nanos = epochNanos(logEventInstant);
- jsonWriterStringBuilder.append(nanos);
- jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 9, '.');
- }
+ private static final EventResolver EPOCH_SECS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_SECONDS.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_SECS_ROUNDED_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getEpochSecond());
- }
+ private static final EventResolver EPOCH_SECS_ROUNDED_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_SECONDS_ROUNDED.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_SECS_NANOS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
- }
+ private static final EventResolver EPOCH_SECS_NANOS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_SECONDS_NANOS.formatTo(buffer, instant);
};
- private static long epochNanos(final Instant instant) {
- final long nanos = Math.multiplyExact(1_000_000_000L, instant.getEpochSecond());
- return Math.addExact(nanos, instant.getNanoOfSecond());
- }
-
static String getName() {
return "timestamp";
}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java
deleted file mode 100644
index 8f7522bb77..0000000000
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.perf.jmh;
-
-import java.time.Instant;
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.TimeZone;
-import java.util.stream.IntStream;
-import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.time.internal.format.FastDatePrinter;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.State;
-import org.openjdk.jmh.infra.Blackhole;
-
-/**
- * Compares {@link MutableInstant} formatting efficiency of
- * {@link FastDatePrinter}, {@link FixedDateFormat}, and {@link DateTimeFormatter}.
- * <p>
- * The major formatting efficiency is mostly provided by caching, i.e.,
- * reusing the earlier formatter output if timestamps match. We deliberately
- * exclude this optimization, since it is applicable to all formatters. This
- * benchmark rather focuses on only and only the formatting efficiency.
- */
-@State(Scope.Thread)
-public class DateTimeFormatBenchmark {
-
- /**
- * The pattern to be tested.
- * <p>
- * Note that neither {@link FastDatePrinter}, nor {@link FixedDateFormat}
- * supports nanosecond precision.
- */
- private static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";
-
- private static final Locale LOCALE = Locale.US;
-
- private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
-
- private static final Instant INIT_INSTANT = Instant.parse("2020-05-14T10:44:23.901Z");
-
- private static final MutableInstant[] INSTANTS = IntStream.range(0, 1_000)
- .mapToObj((final int index) -> {
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(
- Math.addExact(INIT_INSTANT.getEpochSecond(), index),
- Math.addExact(INIT_INSTANT.getNano(), index));
- return instant;
- })
- .toArray(MutableInstant[]::new);
-
- private static final Calendar[] CALENDARS = Arrays.stream(INSTANTS)
- .map((final MutableInstant instant) -> {
- final Calendar calendar = Calendar.getInstance(TIME_ZONE, LOCALE);
- calendar.setTimeInMillis(instant.getEpochMillisecond());
- return calendar;
- })
- .toArray(Calendar[]::new);
-
- private static final FastDatePrinter FAST_DATE_PRINTER = new FastDatePrinter(PATTERN, TIME_ZONE, LOCALE) {};
-
- private static final FixedDateFormat FIXED_DATE_FORMAT = Objects.requireNonNull(
- FixedDateFormat.createIfSupported(PATTERN, TIME_ZONE.getID()),
- "couldn't create FixedDateTime for pattern " + PATTERN + " and time zone " + TIME_ZONE.getID());
-
- private static final DateTimeFormatter DATE_TIME_FORMATTER =
- DateTimeFormatter.ofPattern(PATTERN).withZone(TIME_ZONE.toZoneId()).withLocale(LOCALE);
-
- private final StringBuilder stringBuilder = new StringBuilder(PATTERN.length() * 2);
-
- private final char[] charBuffer = new char[stringBuilder.capacity()];
-
- @Benchmark
- public void fastDatePrinter(final Blackhole blackhole) {
- for (final Calendar calendar : CALENDARS) {
- stringBuilder.setLength(0);
- FAST_DATE_PRINTER.format(calendar, stringBuilder);
- blackhole.consume(stringBuilder.length());
- }
- }
-
- @Benchmark
- public void fixedDateFormat(final Blackhole blackhole) {
- for (final MutableInstant instant : INSTANTS) {
- final int length = FIXED_DATE_FORMAT.formatInstant(instant, charBuffer, 0);
- blackhole.consume(length);
- }
- }
-
- @Benchmark
- public void dateTimeFormatter(final Blackhole blackhole) {
- for (final MutableInstant instant : INSTANTS) {
- stringBuilder.setLength(0);
- DATE_TIME_FORMATTER.formatTo(instant, stringBuilder);
- blackhole.consume(stringBuilder.length());
- }
- }
-}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
deleted file mode 100644
index e22ffa31c7..0000000000
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.perf.jmh;
-
-import java.text.SimpleDateFormat;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.BenchmarkMode;
-import org.openjdk.jmh.annotations.Mode;
-import org.openjdk.jmh.annotations.OutputTimeUnit;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.State;
-
-/**
- * Tests performance of various DateFormatters in a thread-safe manner.
- */
-// ============================== HOW TO RUN THIS TEST: ====================================
-//
-// single thread:
-// java -jar log4j-perf/target/benchmarks.jar ".*ThreadsafeDateFormat.*" -f 1 -wi 5 -i 10
-//
-// multiple threads (for example, 4 threads):
-// java -jar log4j-perf/target/benchmarks.jar ".*ThreadsafeDateFormat.*" -f 1 -wi 5 -i 10 -t 4 -si true
-//
-// Usage help:
-// java -jar log4j-perf/target/benchmarks.jar -help
-//
-@State(Scope.Benchmark)
-public class ThreadsafeDateFormatBenchmark {
-
- private final Date date = new Date();
- private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
- private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
- private final ThreadLocal<SimpleDateFormat> threadLocalSDFormat = new ThreadLocal<SimpleDateFormat>() {
- @Override
- protected SimpleDateFormat initialValue() {
- return new SimpleDateFormat("HH:mm:ss.SSS");
- }
- };
-
- private final ThreadLocal<FormatterSimple> threadLocalCachedSDFormat = new ThreadLocal<FormatterSimple>() {
- @Override
- protected FormatterSimple initialValue() {
- return new FormatterSimple();
- }
- };
-
- private final FastDateFormat fastDateFormat = FastDateFormat.getInstance("HH:mm:ss.SSS");
- private final FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported("HH:mm:ss.SSS");
- private final FormatterFixedReuseBuffer formatFixedReuseBuffer = new FormatterFixedReuseBuffer();
-
- private class CachedTimeFastFormat {
- private final long timestamp;
- private final String formatted;
-
- public CachedTimeFastFormat(final long timestamp) {
- this.timestamp = timestamp;
- this.formatted = fastDateFormat.format(timestamp);
- }
- }
-
- private class CachedTimeFixedFmt {
- private final long timestamp;
- private final String formatted;
-
- public CachedTimeFixedFmt(final long timestamp) {
- this.timestamp = timestamp;
- this.formatted = fixedDateFormat.format(timestamp);
- }
- }
-
- private class FormatterSimple {
- private final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.SSS");
- private long timestamp;
- private String formatted;
-
- public FormatterSimple() {
- this.timestamp = 0;
- }
-
- public String format(final long timestamp) {
- if (timestamp != this.timestamp) {
- this.timestamp = timestamp;
- formatted = format.format(timestamp);
- }
- return formatted;
- }
- }
-
- private class FormatterFixedReuseBuffer {
- private final FixedDateFormat customFormat = FixedDateFormat.createIfSupported("HH:mm:ss.SSS");
- private long timestamp;
- private String formatted;
- private final ThreadLocal<char[]> reusableBuffer = new ThreadLocal<char[]>() {
- @Override
- protected char[] initialValue() {
- return new char[255];
- }
- };
-
- public FormatterFixedReuseBuffer() {
- this.timestamp = 0;
- }
-
- public String format(final long timestamp) {
- if (timestamp != this.timestamp) {
- this.timestamp = timestamp;
- final char[] buffer = reusableBuffer.get();
- final int len = customFormat.format(timestamp, buffer, 0);
- formatted = new String(buffer, 0, len);
- }
- return formatted;
- }
- }
-
- private final long currentTimestamp = 0;
- private String cachedTime = null;
-
- private final AtomicReference<CachedTimeFastFormat> cachedTimeFastFmt =
- new AtomicReference<>(new CachedTimeFastFormat(System.currentTimeMillis()));
- private final AtomicReference<CachedTimeFixedFmt> cachedTimeFixedFmt =
- new AtomicReference<>(new CachedTimeFixedFmt(System.currentTimeMillis()));
-
- public static void main(final String[] args) {}
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public void baseline() {}
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String synchronizedSimpleDateFmt() {
- final long timestamp = System.currentTimeMillis();
- synchronized (simpleDateFormat) {
- if (timestamp != currentTimestamp) {
- cachedTime = simpleDateFormat.format(date);
- }
- return cachedTime;
- }
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String dateTimeFormatter() {
- final LocalDateTime now = LocalDateTime.now();
- return dateTimeFormatter.format(now);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String threadLocalSimpleDateFmt() {
- final long timestamp = System.currentTimeMillis();
- return threadLocalSDFormat.get().format(timestamp);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String cachedThrdLocalSimpleDateFmt() {
- final long timestamp = System.currentTimeMillis();
- return threadLocalCachedSDFormat.get().format(timestamp);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String cachedThrdLocalCustomFormat() {
- final long timestamp = System.currentTimeMillis();
- return formatFixedReuseBuffer.format(timestamp);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fastDateFormat() {
- return fastDateFormat.format(System.currentTimeMillis());
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedDateFormat() {
- return fixedDateFormat.format(System.currentTimeMillis());
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String atomicFastFormat() {
- final long timestamp = System.currentTimeMillis();
- final CachedTimeFastFormat current = cachedTimeFastFmt.get();
- if (timestamp != current.timestamp) {
- final CachedTimeFastFormat newTime = new CachedTimeFastFormat(timestamp);
- if (cachedTimeFastFmt.compareAndSet(current, newTime)) {
- return newTime.formatted;
- }
- return cachedTimeFastFmt.get().formatted;
- }
- return current.formatted;
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String atomicFixedFormat() {
- final long timestamp = System.currentTimeMillis();
- final CachedTimeFixedFmt current = cachedTimeFixedFmt.get();
- if (timestamp != current.timestamp) {
- final CachedTimeFixedFmt newTime = new CachedTimeFixedFmt(timestamp);
- if (cachedTimeFixedFmt.compareAndSet(current, newTime)) {
- return newTime.formatted;
- }
- return cachedTimeFixedFmt.get().formatted;
- }
- return current.formatted;
- }
-}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
deleted file mode 100644
index 90d970f4bc..0000000000
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.perf.jmh;
-
-import java.nio.ByteBuffer;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.BenchmarkMode;
-import org.openjdk.jmh.annotations.Mode;
-import org.openjdk.jmh.annotations.OutputTimeUnit;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.State;
-
-/**
- * Tests performance of various time format implementation.
- */
-// ============================== HOW TO RUN THIS TEST: ====================================
-//
-// single thread:
-// java -jar log4j-perf/target/benchmarks.jar ".*TimeFormat.*" -f 1 -wi 5 -i 10
-//
-// multiple threads (for example, 4 threads):
-// java -jar log4j-perf/target/benchmarks.jar ".*TimeFormat.*" -f 1 -wi 5 -i 5 -t 4 -si true
-//
-// Usage help:
-// java -jar log4j-perf/target/benchmarks.jar -help
-//
-@State(Scope.Benchmark)
-public class TimeFormatBenchmark {
-
- ThreadLocal<SimpleDateFormat> threadLocalSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
- @Override
- protected SimpleDateFormat initialValue() {
- return new SimpleDateFormat("HH:mm:ss.SSS");
- }
- };
- FastDateFormat fastDateFormat = FastDateFormat.getInstance("HH:mm:ss.SSS");
- FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(new String[] {"ABSOLUTE"});
- volatile long midnightToday;
- volatile long midnightTomorrow;
-
- @State(Scope.Thread)
- public static class BufferState {
- final ByteBuffer buffer = ByteBuffer.allocate(12);
- final StringBuilder stringBuilder = new StringBuilder(12);
- final char[] charArray = new char[12];
- }
-
- private long millisSinceMidnight(final long now) {
- if (now >= midnightTomorrow) {
- midnightToday = calcMidnightMillis(now, 0);
- midnightTomorrow = calcMidnightMillis(now, 1);
- }
- return now - midnightToday;
- }
-
- private long calcMidnightMillis(final long time, final int addDays) {
- final Calendar cal = Calendar.getInstance();
- cal.setTimeInMillis(time);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- cal.add(Calendar.DATE, addDays);
- return cal.getTimeInMillis();
- }
-
- public static void main(final String[] args) {
- System.out.println(new TimeFormatBenchmark().fixedBitFiddlingReuseCharArray(new BufferState()));
- System.out.println(new TimeFormatBenchmark().fixedFormatReuseStringBuilder(new BufferState()));
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String simpleDateFormat() {
- return threadLocalSimpleDateFormat.get().format(new Date());
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fastDateFormatCreateNewStringBuilder() {
- return fastDateFormat.format(new Date());
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fastDateFormatReuseStringBuilder(final BufferState state) {
- state.stringBuilder.setLength(0);
- fastDateFormat.format(new Date(), state.stringBuilder);
- return new String(state.stringBuilder);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedBitFiddlingReuseCharArray(final BufferState state) {
- final int len = formatCharArrayBitFiddling(System.currentTimeMillis(), state.charArray, 0);
- return new String(state.charArray, 0, len);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedDateFormatCreateNewCharArray(final BufferState state) {
- return fixedDateFormat.format(System.currentTimeMillis());
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedDateFormatReuseCharArray(final BufferState state) {
- final int len = fixedDateFormat.format(System.currentTimeMillis(), state.charArray, 0);
- return new String(state.charArray, 0, len);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedFormatReuseStringBuilder(final BufferState state) {
- state.stringBuilder.setLength(0);
- formatStringBuilder(System.currentTimeMillis(), state.stringBuilder);
- return new String(state.stringBuilder);
- }
-
- int formatCharArrayBitFiddling(final long time, final char[] buffer, final int pos) {
- // Calculate values by getting the ms values first and do then
- // shave off the hour minute and second values with multiplications
- // and bit shifts instead of simple but expensive divisions.
-
- // Get daytime in ms which does fit into an int
- // int ms = (int) (time % 86400000);
- int ms = (int) (millisSinceMidnight(time));
-
- // well ... it works
- final int hour = (int) (((ms >> 7) * 9773437L) >> 38);
- ms -= 3600000 * hour;
-
- final int minute = (int) (((ms >> 5) * 2290650L) >> 32);
- ms -= 60000 * minute;
-
- final int second = ((ms >> 3) * 67109) >> 23;
- ms -= 1000 * second;
-
- // Hour
- // 13/128 is nearly the same as /10 for values up to 65
- int temp = (hour * 13) >> 7;
- int p = pos;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (hour - 10 * temp + '0'));
- buffer[p++] = ((char) ':');
-
- // Minute
- // 13/128 is nearly the same as /10 for values up to 65
- temp = (minute * 13) >> 7;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (minute - 10 * temp + '0'));
- buffer[p++] = ((char) ':');
-
- // Second
- // 13/128 is nearly the same as /10 for values up to 65
- temp = (second * 13) >> 7;
- buffer[p++] = ((char) (temp + '0'));
- buffer[p++] = ((char) (second - 10 * temp + '0'));
- buffer[p++] = ((char) '.');
-
- // Millisecond
- // 41/4096 is nearly the same as /100
- temp = (ms * 41) >> 12;
- buffer[p++] = ((char) (temp + '0'));
-
- ms -= 100 * temp;
- temp = (ms * 205) >> 11; // 205/2048 is nearly the same as /10
- buffer[p++] = ((char) (temp + '0'));
-
- ms -= 10 * temp;
- buffer[p++] = ((char) (ms + '0'));
- return p;
- }
-
- StringBuilder formatStringBuilder(final long time, final StringBuilder buffer) {
- // Calculate values by getting the ms values first and do then
- // calculate the hour minute and second values divisions.
-
- // Get daytime in ms which does fit into an int
- // int ms = (int) (time % 86400000);
- int ms = (int) (millisSinceMidnight(time));
-
- final int hours = ms / 3600000;
- ms -= 3600000 * hours;
-
- final int minutes = ms / 60000;
- ms -= 60000 * minutes;
-
- final int seconds = ms / 1000;
- ms -= 1000 * seconds;
-
- // Hour
- int temp = hours / 10;
- buffer.append((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer.append((char) (hours - 10 * temp + '0'));
- buffer.append((char) ':');
-
- // Minute
- temp = minutes / 10;
- buffer.append((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer.append((char) (minutes - 10 * temp + '0'));
- buffer.append((char) ':');
-
- // Second
- temp = seconds / 10;
- buffer.append((char) (temp + '0'));
- buffer.append((char) (seconds - 10 * temp + '0'));
- buffer.append((char) '.');
-
- // Millisecond
- temp = ms / 100;
- buffer.append((char) (temp + '0'));
-
- ms -= 100 * temp;
- temp = ms / 10;
- buffer.append((char) (temp + '0'));
-
- ms -= 10 * temp;
- buffer.append((char) (ms + '0'));
- return buffer;
- }
-
- int formatCharArray(final long time, final char[] buffer, final int pos) {
- // Calculate values by getting the ms values first and do then
- // calculate the hour minute and second values divisions.
-
- // Get daytime in ms which does fit into an int
- // int ms = (int) (time % 86400000);
- int ms = (int) (millisSinceMidnight(time));
-
- final int hours = ms / 3600000;
- ms -= 3600000 * hours;
-
- final int minutes = ms / 60000;
- ms -= 60000 * minutes;
-
- final int seconds = ms / 1000;
- ms -= 1000 * seconds;
-
- // Hour
- int temp = hours / 10;
- int p = pos;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (hours - 10 * temp + '0'));
- buffer[p++] = ((char) ':');
-
- // Minute
- temp = minutes / 10;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (minutes - 10 * temp + '0'));
- buffer[p++] = ((char) ':');
-
- // Second
- temp = seconds / 10;
- buffer[p++] = ((char) (temp + '0'));
- buffer[p++] = ((char) (seconds - 10 * temp + '0'));
- buffer[p++] = ((char) '.');
-
- // Millisecond
- temp = ms / 100;
- buffer[p++] = ((char) (temp + '0'));
-
- ms -= 100 * temp;
- temp = ms / 10;
- buffer[p++] = ((char) (temp + '0'));
-
- ms -= 10 * temp;
- buffer[p++] = ((char) (ms + '0'));
- return p;
- }
-}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java
new file mode 100644
index 0000000000..d46c431ea0
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.perf.jmh.instant;
+
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.stream.IntStream;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Compares {@link DateTimeFormatter} efficiency for formatting the {@code ss.SSS} singleton versus formatting the {@code ss}, {@code .}, and {@code SSS} sequence.
+ * This comparison is influential on the sequence merging strategies of {@code InstantPatternDynamicFormatter}.
+ */
+@State(Scope.Thread)
+public class InstantPatternDynamicFormatterSequencingBenchmark {
+
+ static final Locale LOCALE = Locale.US;
+
+ static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
+
+ private static final Instant[] INSTANTS = createInstants();
+
+ private static Instant[] createInstants() {
+ final Instant initInstant = Instant.parse("2020-05-14T10:44:23.901Z");
+ return IntStream.range(0, 1_000)
+ .mapToObj((final int index) -> Instant.ofEpochSecond(
+ Math.addExact(initInstant.getEpochSecond(), index),
+ Math.addExact(initInstant.getNano(), index)))
+ .toArray(Instant[]::new);
+ }
+
+ @FunctionalInterface
+ private interface Formatter {
+
+ void formatTo(TemporalAccessor instantAccessor, StringBuilder buffer);
+ }
+
+ private static final Formatter SINGLETON_FORMATTER =
+ DateTimeFormatter.ofPattern("ss.SSS").withLocale(LOCALE).withZone(TIME_ZONE.toZoneId())::formatTo;
+
+ private static final Formatter SEQUENCED_FORMATTER = new Formatter() {
+
+ private final Formatter[] formatters = {
+ DateTimeFormatter.ofPattern("ss").withLocale(LOCALE).withZone(TIME_ZONE.toZoneId())::formatTo,
+ (temporal, appendable) -> appendable.append("."),
+ DateTimeFormatter.ofPattern("SSS").withLocale(LOCALE).withZone(TIME_ZONE.toZoneId())::formatTo
+ };
+
+ @Override
+ public void formatTo(final TemporalAccessor instantAccessor, final StringBuilder buffer) {
+ for (Formatter formatter : formatters) {
+ formatter.formatTo(instantAccessor, buffer);
+ }
+ }
+ };
+
+ private final StringBuilder buffer = new StringBuilder();
+
+ @Benchmark
+ public void singleton(final Blackhole blackhole) {
+ benchmark(blackhole, SINGLETON_FORMATTER);
+ }
+
+ @Benchmark
+ public void sequenced(final Blackhole blackhole) {
+ benchmark(blackhole, SEQUENCED_FORMATTER);
+ }
+
+ private void benchmark(final Blackhole blackhole, final Formatter formatter) {
+ for (final Instant instant : INSTANTS) {
+ formatter.formatTo(instant, buffer);
+ blackhole.consume(buffer);
+ buffer.setLength(0);
+ }
+ }
+}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
new file mode 100644
index 0000000000..0506318acd
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.perf.jmh.instant;
+
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Compares {@link MutableInstant} formatting efficiency of {@link InstantPatternFormatter} and {@link DateTimeFormatter}.
+ * <p>
+ * The major formatting efficiency is mostly provided by caching, i.e., reusing the earlier formatter output if timestamps match.
+ * We deliberately exclude this optimization (by means of always distinct instants), since it is applicable to all formatters.
+ * This benchmark rather focuses on only and only the formatting efficiency.
+ * </p>
+ *
+ * @see InstantPatternFormatterImpactBenchmark for the performance impact of different date & time formatters on a typical layout
+ */
+@State(Scope.Thread)
+public class InstantPatternFormatterBenchmark {
+
+ static final Locale LOCALE = Locale.US;
+
+ static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
+
+ private static final MutableInstant[] INSTANTS = createInstants();
+
+ private static MutableInstant[] createInstants() {
+ final Instant initInstant = Instant.parse("2020-05-14T10:44:23.901Z");
+ MutableInstant[] instants = IntStream.range(0, 1_000)
+ .mapToObj((final int index) -> {
+ final Instant instant = initInstant.plusMillis(index).plusNanos(1);
+ final MutableInstant mutableInstant = new MutableInstant();
+ mutableInstant.initFromEpochSecond(instant.getEpochSecond(), instant.getNano());
+ return mutableInstant;
+ })
+ .toArray(MutableInstant[]::new);
+ validateInstants(instants);
+ return instants;
+ }
+
+ @SuppressWarnings("OptionalGetWithoutIsPresent")
+ static <I extends org.apache.logging.log4j.core.time.Instant> void validateInstants(final I[] instants) {
+
+ // Find the instant offset
+ final Supplier<LongStream> millisStreamSupplier = () ->
+ Arrays.stream(instants).mapToLong(org.apache.logging.log4j.core.time.Instant::getEpochMillisecond);
+ final long minMillis = millisStreamSupplier.get().min().getAsLong();
+ final long maxMillis = millisStreamSupplier.get().max().getAsLong();
+ final long offMillis = maxMillis - minMillis;
+
+ // Validate for `InstantPatternDynamicFormatter`
+ if (TimeUnit.MINUTES.toMillis(1) <= offMillis) {
+ final String message = String.format(
+ "instant samples must be of the same week to exploit the `%s` caching",
+ InstantPatternFormatter.class.getSimpleName());
+ throw new IllegalStateException(message);
+ }
+ }
+
+ private static final Formatters DATE_TIME_FORMATTERS = new Formatters("yyyy-MM-dd'T'HH:mm:ss.SSS");
+
+ private static final Formatters TIME_FORMATTERS = new Formatters("HH:mm:ss.SSS");
+
+ static final class Formatters {
+
+ private final String pattern;
+
+ final InstantPatternFormatter instantFormatter;
+
+ final DateTimeFormatter javaFormatter;
+
+ Formatters(final String pattern) {
+ this.pattern = pattern;
+ this.instantFormatter = InstantPatternFormatter.newBuilder()
+ .setPattern(pattern)
+ .setLocale(LOCALE)
+ .setTimeZone(TIME_ZONE)
+ .setCachingEnabled(false)
+ .build();
+ this.javaFormatter = DateTimeFormatter.ofPattern(pattern)
+ .withZone(TIME_ZONE.toZoneId())
+ .withLocale(LOCALE);
+ }
+ }
+
+ private final StringBuilder stringBuilder =
+ new StringBuilder(Math.max(DATE_TIME_FORMATTERS.pattern.length(), TIME_FORMATTERS.pattern.length()) * 2);
+
+ @Benchmark
+ public void instantFormatter_dateTime(final Blackhole blackhole) {
+ instantFormatter(blackhole, DATE_TIME_FORMATTERS.instantFormatter);
+ }
+
+ @Benchmark
+ public void instantFormatter_time(final Blackhole blackhole) {
+ instantFormatter(blackhole, TIME_FORMATTERS.instantFormatter);
+ }
+
+ private void instantFormatter(final Blackhole blackhole, final InstantPatternFormatter formatter) {
+ for (final MutableInstant instant : INSTANTS) {
+ stringBuilder.setLength(0);
+ formatter.formatTo(stringBuilder, instant);
+ blackhole.consume(stringBuilder.length());
+ }
+ }
+
+ @Benchmark
+ public void javaFormatter_dateTime(final Blackhole blackhole) {
+ javaFormatter(blackhole, DATE_TIME_FORMATTERS.javaFormatter);
+ }
+
+ @Benchmark
+ public void javaFormatter_time(final Blackhole blackhole) {
+ javaFormatter(blackhole, TIME_FORMATTERS.javaFormatter);
+ }
+
+ private void javaFormatter(final Blackhole blackhole, final DateTimeFormatter formatter) {
+ for (final MutableInstant instant : INSTANTS) {
+ stringBuilder.setLength(0);
+ formatter.formatTo(instant, stringBuilder);
+ blackhole.consume(stringBuilder.length());
+ }
+ }
+}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java
new file mode 100644
index 0000000000..53645ac7e6
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.perf.jmh.instant;
+
+import static org.apache.logging.log4j.perf.jmh.instant.InstantPatternFormatterBenchmark.validateInstants;
+
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.function.BiFunction;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.NullConfiguration;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
+import org.apache.logging.log4j.layout.template.json.LogEventFixture;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Benchmarks the impact of different date & time formatters on a typical layout.
+ *
+ * @see InstantPatternFormatterBenchmark for isolated benchmarks of date & time formatters
+ */
+@State(Scope.Thread)
+@SuppressWarnings("deprecation")
+public class InstantPatternFormatterImpactBenchmark {
+
+ private static final List<LogEvent> LITE_LOG_EVENTS = createLogEvents(LogEventFixture::createLiteLogEvents);
+
+ private static final List<LogEvent> FULL_LOG_EVENTS = createLogEvents(LogEventFixture::createFullLogEvents);
+
+ private static List<LogEvent> createLogEvents(final BiFunction<Integer, Integer, List<LogEvent>> supplier) {
+ final int logEventCount = 1_000;
+ final List<LogEvent> logEvents = supplier.apply(
+ logEventCount,
+ // Avoid overlapping instants to ensure the impact of date & time formatting at event encoding:
+ 1);
+ final Instant[] instants = logEvents.stream().map(LogEvent::getInstant).toArray(Instant[]::new);
+ validateInstants(instants);
+ return logEvents;
+ }
+
+ private static final PatternLayout LAYOUT = PatternLayout.newBuilder()
+ .setConfiguration(new NullConfiguration())
+ // Use a typical pattern *without* a date & time converter!
+ .setPattern("[%t] %p %-40.40c{1.} %notEmpty{%x }- %m%n")
+ .setAlwaysWriteExceptions(true)
+ .build();
+
+ private static final InstantPatternFormatterBenchmark.Formatters FORMATTERS =
+ new InstantPatternFormatterBenchmark.Formatters("yyyy-MM-dd'T'HH:mm:ss.SSS");
+
+ private final StringBuilder stringBuilder = new StringBuilder(1_1024 * 16);
+
+ @Benchmark
+ public void instantFormatter_lite(final Blackhole blackhole) {
+ instantFormatter(blackhole, LITE_LOG_EVENTS, FORMATTERS.instantFormatter);
+ }
+
+ @Benchmark
+ public void instantFormatter_full(final Blackhole blackhole) {
+ instantFormatter(blackhole, FULL_LOG_EVENTS, FORMATTERS.instantFormatter);
+ }
+
+ private void instantFormatter(
+ final Blackhole blackhole, final List<LogEvent> logEvents, final InstantPatternFormatter formatter) {
+ // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int logEventIndex = 0; logEventIndex < logEvents.size(); logEventIndex++) {
+
+ // 1. Encode event
+ final LogEvent logEvent = logEvents.get(logEventIndex);
+ stringBuilder.setLength(0);
+ LAYOUT.serialize(logEvent, stringBuilder);
+
+ // 2. Encode date & time
+ final MutableInstant instant = (MutableInstant) logEvent.getInstant();
+ formatter.formatTo(stringBuilder, instant);
+ blackhole.consume(stringBuilder.length());
+ }
+ }
+
+ @Benchmark
+ public void javaFormatter_lite(final Blackhole blackhole) {
+ javaFormatter(blackhole, LITE_LOG_EVENTS, FORMATTERS.javaFormatter);
+ }
+
+ @Benchmark
+ public void javaFormatter_full(final Blackhole blackhole) {
+ javaFormatter(blackhole, FULL_LOG_EVENTS, FORMATTERS.javaFormatter);
+ }
+
+ private void javaFormatter(
+ final Blackhole blackhole, final List<LogEvent> logEvents, final DateTimeFormatter formatter) {
+ // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int logEventIndex = 0; logEventIndex < logEvents.size(); logEventIndex++) {
+
+ // 1. Encode event
+ final LogEvent logEvent = logEvents.get(logEventIndex);
+ stringBuilder.setLength(0);
+ LAYOUT.serialize(logEvent, stringBuilder);
+
+ // 2. Encode date & time
+ final MutableInstant instant = (MutableInstant) logEvent.getInstant();
+ formatter.formatTo(instant, stringBuilder);
+ blackhole.consume(stringBuilder.length());
+ }
+ }
+}
diff --git a/src/changelog/.3.x.x/3149_remove_named_date_patterns.xml b/src/changelog/.3.x.x/3149_remove_named_date_patterns.xml
new file mode 100644
index 0000000000..38c2bd8ae3
--- /dev/null
+++ b/src/changelog/.3.x.x/3149_remove_named_date_patterns.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="https://logging.apache.org/xml/ns"
+ xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="removed">
+ <issue id="3149" link="https://github.com/apache/logging-log4j2/pull/3149"/>
+ <description format="asciidoc">Remove _named_ date & time formatting patterns in Pattern Layout, except for `UNIX` and `UNIX_MILLIS`</description>
+</entry>
diff --git a/src/changelog/.index.adoc.ftl b/src/changelog/.index.adoc.ftl
index b71f9905f3..24c1ef3895 100644
--- a/src/changelog/.index.adoc.ftl
+++ b/src/changelog/.index.adoc.ftl
@@ -37,7 +37,7 @@
:page-toclevels: 1
[#release-notes]
-= Release Notes
+= Release notes
<#list releases as release><#if release.changelogEntryCount gt 0>
include::_release-notes/${release.version}.adoc[]
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index c754462477..ef0a1c1c26 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -387,7 +387,7 @@ See xref:manual/layouts.adoc#LocationInformation[this section of the layouts pag
[#converter-date]
==== Date
-Outputs the date of the log event
+Outputs the instant of the log event
.link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/DatePatternConverter.html[`DatePatternConverter`] specifier grammar
[source,text]
@@ -396,8 +396,7 @@ d{pattern}
date{pattern}
----
-The date conversion specifier may be followed by a set of braces containing a date and time pattern string per
-https://docs.oracle.com/en/java/javase/{java-target-version}/docs/api/java.base/java/text/SimpleDateFormat.html[`SimpleDateFormat`].
+The date conversion specifier may be followed by a set of braces containing a date and time formatting pattern per https://docs.oracle.com/en/java/javase/{java-target-version}/docs/api/java.base/java/time/format/DateTimeFormatter.html[`DateTimeFormatter`].
The predefined _named_ formats are:
[%header,cols="2m,3m"]
@@ -405,45 +404,6 @@ The predefined _named_ formats are:
|Pattern
|Example output
-|%d\{DEFAULT}
-|2012-11-02 14:34:02,123
-
-|%d\{DEFAULT_MICROS}
-|2012-11-02 14:34:02,123456
-
-|%d\{DEFAULT_NANOS}
-|2012-11-02 14:34:02,123456789
-
-|%d\{ISO8601}
-|2012-11-02T14:34:02,781
-
-|%d\{ISO8601_BASIC}
-|20121102T143402,781
-
-|%d\{ISO8601_OFFSET_DATE_TIME_HH}
-|2012-11-02'T'14:34:02,781-07
-
-|%d\{ISO8601_OFFSET_DATE_TIME_HHMM}
-|2012-11-02'T'14:34:02,781-0700
-
-|%d\{ISO8601_OFFSET_DATE_TIME_HHCMM}
-|2012-11-02'T'14:34:02,781-07:00
-
-|%d\{ABSOLUTE}
-|14:34:02,781
-
-|%d\{ABSOLUTE_MICROS}
-|14:34:02,123456
-
-|%d\{ABSOLUTE_NANOS}
-|14:34:02,123456789
-
-|%d\{DATE}
-|02 Nov 2012 14:34:02,781
-
-|%d\{COMPACT}
-|20121102143402781
-
|%d\{UNIX}
|1351866842
@@ -453,7 +413,7 @@ The predefined _named_ formats are:
You can also use a set of braces containing a time zone id per
https://docs.oracle.com/en/java/javase/{java-target-version}/docs/api/java.base/java/util/TimeZone.html#getTimeZone(java.lang.String)[`java.util.TimeZone#getTimeZone(String)`].
-If no date format specifier is given then the `DEFAULT` format is used.
+If no date format specifier is given, then the `yyyy-MM-dd HH:mm:ss.SSS` pattern is used.
You can also define custom date formats, see following examples:
@@ -465,36 +425,19 @@ You can also define custom date formats, see following examples:
|%d{HH:mm:ss,SSS}
|14:34:02,123
-|%d{HH:mm:ss,nnnn} to %d{HH:mm:ss,nnnnnnnnn}
-|14:34:02,1234 to 14:34:02,123456789
-
-|%d{dd MMM yyyy HH:mm:ss,SSS}
-|02 Nov 2012 14:34:02,123
-
-|%d{dd MMM yyyy HH:mm:ss,nnnn} to %d{dd MMM yyyy HH:mm:ss,nnnnnnnnn}
-|02 Nov 2012 14:34:02,1234 to 02 Nov 2012 14:34:02,123456789
-
-|%d{HH:mm:ss}{GMT+0}
-|18:34:02
+|%d{yyyy-mm-dd'T'HH:mm:ss.SSS'Z'}\{UTC}
+|2012-11-02T14:34:02.123Z
|===
-`%d\{UNIX}` outputs the UNIX time in seconds.
-`%d\{UNIX_MILLIS}` outputs the UNIX time in milliseconds.
-The `UNIX` time is the difference – in seconds for `UNIX` and in milliseconds for `UNIX_MILLIS` – between the current time and 1970-01-01 00:00:00 (UTC).
-While the time unit is milliseconds, the granularity depends on the platform.
-This is an efficient way to output the event time because only a conversion from `long` to `String` takes place, there is no `Date` formatting involved.
-
-There is also limited support for timestamps more precise than milliseconds when running on Java 9 or later.
-Note that not all
-https://docs.oracle.com/en/java/javase/{java-target-version}/docs/api/java.base/java/time/format/DateTimeFormatter.html[`DateTimeFormatter`]
-formats are supported.
-Only timestamps in the formats mentioned in the table above may use the _nano-of-second_ pattern letter `n` instead of the _fraction-of-second_ pattern letter `S`.
+`%d\{UNIX}` outputs the epoch time in seconds, i.e., the difference in seconds between the current time and 1970-01-01 00:00:00 (UTC).
+`%d\{UNIX_MILLIS}` outputs the epoch time in milliseconds.
-Users may revert to a millisecond-precision clock when running on Java 9 by setting xref:manual/systemproperties.adoc#log4j.configuration.clock[the `log4j.configuration.clock` system property] to `SystemMillisClock`.
+Note that the granularity of the sub-second formatters depends on the platform.
+Users may revert to a millisecond-precision clock when running on Java 9 by setting xref:manual/systemproperties.adoc#log4j2.clock[the `log4j2.clock` system property] to `SystemMillisClock`.
[WARNING]
====
-Only named date formats (`DEFAULT`, `ISO8601`, `UNIX`, `UNIX_MILLIS`, etc.) are garbage-free.
+Except `UNIX` and `UNIX_MILLIS` named patterns, the rest of the date & time formatters are not garbage-free.
====
[#converter-encode]