diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/FileAppenderTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/FileAppenderTest.java index 72cbc775d93..edf22c32af2 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/FileAppenderTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/FileAppenderTest.java @@ -265,7 +265,7 @@ private static void writer( private void verifyFile(final int count) throws Exception { // String expected = "[\\w]* \\[\\s*\\] INFO TestLogger - Test$"; final String expected = - "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3} \\[[^\\]]*\\] INFO TestLogger - Test"; + "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3} \\[[^\\]]*\\] INFO TestLogger - Test"; final Pattern pattern = Pattern.compile(expected); int lines = 0; try (final BufferedReader is = new BufferedReader(new InputStreamReader(new FileInputStream(FILE_NAME)))) { 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 0254773222a..df7d585e3d6 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 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 void testPolicy() throws Exception { 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 51fead166a2..423e91fd639 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 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 void testAppender(final LoggerContext context) throws Exception { 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 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 0fd8772135a..d0e2153585b 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 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 void testAppender(final LoggerContext context) throws Exception { 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 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 15240841f93..f0fdffc7581 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 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 void testAppender(final LoggerContext context) throws Exception { 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 38f7417a95d..a8133732b05 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 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 void testAppender(final LoggerContext context) throws Exception { 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 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/config/plugins/LegacyPluginTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/LegacyPluginTest.java deleted file mode 100644 index 8edd3867a52..00000000000 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/LegacyPluginTest.java +++ /dev/null @@ -1,55 +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.config.plugins; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.Map; -import org.apache.logging.log4j.core.Appender; -import org.apache.logging.log4j.core.Layout; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.xml.XmlConfiguration; -import org.apache.logging.log4j.core.test.junit.LoggerContextSource; -import org.junit.jupiter.api.Test; - -@LoggerContextSource("legacy-plugins.xml") -public class LegacyPluginTest { - - @Test - public void testLegacy(final Configuration configuration) throws Exception { - assertThat(configuration, instanceOf(XmlConfiguration.class)); - for (Map.Entry entry : configuration.getAppenders().entrySet()) { - if (entry.getKey().equalsIgnoreCase("console")) { - final Layout layout = entry.getValue().getLayout(); - assertNotNull("No layout for Console Appender"); - final String name = layout.getClass().getSimpleName(); - assertEquals("LogstashLayout", name, "Incorrect Layout class. Expected LogstashLayout, Actual " + name); - } else if (entry.getKey().equalsIgnoreCase("customConsole")) { - final Layout layout = entry.getValue().getLayout(); - assertNotNull("No layout for CustomConsole Appender"); - final String name = layout.getClass().getSimpleName(); - assertEquals( - "CustomConsoleLayout", - name, - "Incorrect Layout class. Expected CustomConsoleLayout, Actual " + name); - } - } - } -} 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 cb14f727eab..bab2f930728 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.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 void testLayoutWithDatePatternUnixMillis() { assertEquals("" + event.getTimeMillis() + "", 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( - "" + expected + "", - 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 d908f8013fe..961a30f61a4 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 @@ 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 @@ public long getTimeMillis() { } } - /** - * 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 @@ private static Date date(final int year, final int month, final int date) { 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 @@ public void testFormatDateStringBuilderOriginalPattern() { @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 @@ public void testFormatObjectStringBuilderDefaultPattern() { @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/pattern/PatternParserTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java index 6c6b763ca96..880775575d1 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java @@ -188,7 +188,7 @@ public void testBadPattern() { final String str = buf.toString(); // eats all characters until the closing '}' character - final String expected = "[2001-02-03 04:05:06,789] - Hello, world"; + final String expected = "[2001-02-03 04:05:06.789] - Hello, world"; assertTrue(str.startsWith(expected), "Expected to start with: " + expected + ". Actual: " + str); } 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 783e4380785..00000000000 --- 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 LOG4J2-3614. - */ - @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 691483c0290..00000000000 --- 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 00000000000..3521d4d4105 --- /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 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 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 00000000000..ddb8c311039 --- /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 expectedSequences) { + final List actualSequences = sequencePattern(pattern, thresholdPrecision); + assertThat(actualSequences).isEqualTo(expectedSequences); + } + + static List sequencingTestCases() { + final List 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 hourPrecisionPatterns() { + final List 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 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 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 00000000000..b25fb85d741 --- /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 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) + InstantPatternThreadLocalCachedFormatter::ofMilliPrecision, + "HH:mm.SSS", + locale1, + timeZone1 + }, + { + (Function) + InstantPatternThreadLocalCachedFormatter::ofMilliPrecision, + "HH:mm.SSS", + locale2, + timeZone2 + }, + // For `ofSecondPrecision()` + { + (Function) + InstantPatternThreadLocalCachedFormatter::ofSecondPrecision, + "yyyy", + locale1, + timeZone1 + }, + { + (Function) + 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/legacy-plugins.xml b/log4j-core-test/src/test/resources/legacy-plugins.xml deleted file mode 100644 index 92a7f330c80..00000000000 --- a/log4j-core-test/src/test/resources/legacy-plugins.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - 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 d109ef9581a..64898bb1dd4 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 @@ + filePattern="target/rolling-size-test/rollingtest-%d{yyyy-MM-dd'T'HH-mm-ss-SSS}.log"> %m%n 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 6459e57cd9f..9212a9bb8eb 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 @@ private HtmlLayout( 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 09cc6c7ddda..d4a0bda04bb 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 @@ @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 mutableInstantRecycler; - - private final Recycler 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 @@ private Formatter createFormatter(final String[] options) { * @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 0e2fbd3efd9..4bdda720551 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 @@ private FileDatePatternConverter(final Configuration configuration, final String * @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 02544f1d71b..00000000000 --- 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. - *

- * 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. - *

- */ -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. - *

- * 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. - *

- */ - 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 7cebb78f601..00000000000 --- 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. - *

- * Warning: Since binary compatible methods may be added to this interface in any - * release, developers are not expected to implement this interface. - *

- * - *

- * Copied and modified from Apache Commons Lang. - *

- * - * @since Apache Commons Lang 3.2 - */ -public interface DatePrinter { - - /** - *

Formats a millisecond {@code long} value.

- * - * @param millis the millisecond value to format - * @return the formatted string - * @since 2.1 - */ - String format(long millis); - - /** - *

Formats a {@code Date} object using a {@code GregorianCalendar}.

- * - * @param date the date to format - * @return the formatted string - */ - String format(Date date); - - /** - *

Formats a {@code Calendar} object.

- * 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); - - /** - *

Formats a millisecond {@code long} value into the - * supplied {@code Appendable}.

- * - * @param millis the millisecond value to format - * @param buf the buffer to format into - * @param the Appendable class type, usually StringBuilder or StringBuffer. - * @return the specified string buffer - * @since 3.5 - */ - B format(long millis, B buf); - - /** - *

Formats a {@code Date} object into the - * supplied {@code Appendable} using a {@code GregorianCalendar}.

- * - * @param date the date to format - * @param buf the buffer to format into - * @param the Appendable class type, usually StringBuilder or StringBuffer. - * @return the specified string buffer - * @since 3.5 - */ - B format(Date date, B buf); - - /** - *

Formats a {@code Calendar} object into the supplied {@code Appendable}.

- * 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 the Appendable class type, usually StringBuilder or StringBuffer. - * @return the specified string buffer - * @since 3.5 - */ - B format(Calendar calendar, B buf); - - // Accessors - // ----------------------------------------------------------------------- - /** - *

Gets the pattern used by this printer.

- * - * @return the pattern, {@link java.text.SimpleDateFormat} compatible - */ - String getPattern(); - - /** - *

Gets the time zone used by this printer.

- * - *

This zone is always used for {@code Date} printing.

- * - * @return the time zone - */ - TimeZone getTimeZone(); - - /** - *

Gets the locale used by this printer.

- * - * @return the locale - */ - Locale getLocale(); - - /** - *

Formats a {@code Date}, {@code Calendar} or - * {@code Long} (milliseconds) object.

- * - * @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 b20046ea06d..00000000000 --- 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; - -/** - *

FastDateFormat is a fast and thread-safe version of - * {@link java.text.SimpleDateFormat}.

- * - *

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)} - *

- * - *

Since FastDateFormat is thread safe, you can use a static member instance:

- * - * private static final FastDateFormat DATE_FORMATTER = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.SHORT); - * - * - *

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. - *

- * - *

All patterns are compatible with - * SimpleDateFormat (except time zones and some year patterns - see below).

- * - *

Since 3.2, FastDateFormat supports parsing as well as printing.

- * - *

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).

- * - *

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.

- * - *

Javadoc cites for the year pattern: For formatting, if the number of - * pattern letters is 2, the year is truncated to 2 digits; otherwise it is - * interpreted as a number. 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.

- * - *

- * Copied and modified from Apache Commons Lang. - *

- * - * @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 cache = new FormatCache() { - @Override - protected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) { - return new FastDateFormat(pattern, timeZone, locale); - } - }; - - private final FastDatePrinter printer; - - // ----------------------------------------------------------------------- - /** - *

Gets a formatter instance using the default pattern in the - * default locale.

- * - * @return a date/time formatter - */ - public static FastDateFormat getInstance() { - return cache.getInstance(); - } - - /** - *

Gets a formatter instance using the specified pattern in the - * default locale.

- * - * @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); - } - - /** - *

Gets a formatter instance using the specified pattern and - * time zone.

- * - * @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); - } - - /** - *

Gets a formatter instance using the specified pattern and - * locale.

- * - * @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); - } - - /** - *

Gets a formatter instance using the specified pattern, time zone - * and locale.

- * - * @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); - } - - // ----------------------------------------------------------------------- - /** - *

Gets a date formatter instance using the specified style in the - * default time zone and locale.

- * - * @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); - } - - /** - *

Gets a date formatter instance using the specified style and - * locale in the default time zone.

- * - * @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); - } - - /** - *

Gets a date formatter instance using the specified style and - * time zone in the default locale.

- * - * @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); - } - - /** - *

Gets a date formatter instance using the specified style, time - * zone and locale.

- * - * @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); - } - - // ----------------------------------------------------------------------- - /** - *

Gets a time formatter instance using the specified style in the - * default time zone and locale.

- * - * @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); - } - - /** - *

Gets a time formatter instance using the specified style and - * locale in the default time zone.

- * - * @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); - } - - /** - *

Gets a time formatter instance using the specified style and - * time zone in the default locale.

- * - * @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); - } - - /** - *

Gets a time formatter instance using the specified style, time - * zone and locale.

- * - * @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); - } - - // ----------------------------------------------------------------------- - /** - *

Gets a date/time formatter instance using the specified style - * in the default time zone and locale.

- * - * @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); - } - - /** - *

Gets a date/time formatter instance using the specified style and - * locale in the default time zone.

- * - * @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); - } - - /** - *

Gets a date/time formatter instance using the specified style and - * time zone in the default locale.

- * - * @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); - } - /** - *

Gets a date/time formatter instance using the specified style, - * time zone and locale.

- * - * @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); - } - - /** - *

Constructs a new FastDateFormat.

- * - * @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 - // ----------------------------------------------------------------------- - /** - *

Constructs a new FastDateFormat.

- * - * @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 - // ----------------------------------------------------------------------- - /** - *

Constructs a new FastDateFormat.

- * - * @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 - // ----------------------------------------------------------------------- - /** - *

Formats a {@code Date}, {@code Calendar} or - * {@code Long} (milliseconds) object.

- * 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)); - } - - /** - *

Formats a millisecond {@code long} value.

- * - * @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); - } - - /** - *

Formats a {@code Date} object using a {@code GregorianCalendar}.

- * - * @param date the date to format - * @return the formatted string - */ - @Override - public String format(final Date date) { - return printer.format(date); - } - - /** - *

Formats a {@code Calendar} object.

- * - * @param calendar the calendar to format - * @return the formatted string - */ - @Override - public String format(final Calendar calendar) { - return printer.format(calendar); - } - - /** - *

Formats a millisecond {@code long} value into the - * supplied {@code StringBuffer}.

- * - * @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 format(final long millis, final B buf) { - return printer.format(millis, buf); - } - - /** - *

Formats a {@code Date} object into the - * supplied {@code StringBuffer} using a {@code GregorianCalendar}.

- * - * @param date the date to format - * @param buf the buffer to format into - * @return the specified string buffer - * @since 3.5 - */ - @Override - public B format(final Date date, final B buf) { - return printer.format(date, buf); - } - - /** - *

Formats a {@code Calendar} object into the - * supplied {@code StringBuffer}.

- * - * @param calendar the calendar to format - * @param buf the buffer to format into - * @return the specified string buffer - * @since 3.5 - */ - @Override - public B format(final Calendar calendar, final B buf) { - return printer.format(calendar, buf); - } - - // Accessors - // ----------------------------------------------------------------------- - /** - *

Gets the pattern used by this formatter.

- * - * @return the pattern, {@link java.text.SimpleDateFormat} compatible - */ - @Override - public String getPattern() { - return printer.getPattern(); - } - - /** - *

Gets the time zone used by this formatter.

- * - *

This zone is always used for {@code Date} formatting.

- * - * @return the time zone - */ - @Override - public TimeZone getTimeZone() { - return printer.getTimeZone(); - } - - /** - *

Gets the locale used by this formatter.

- * - * @return the locale - */ - @Override - public Locale getLocale() { - return printer.getLocale(); - } - - /** - *

Gets an estimate for the maximum string length that the - * formatter will produce.

- * - *

The actual formatted length will almost always be less than or - * equal to this amount.

- * - * @return the maximum formatted length - */ - public int getMaxLengthEstimate() { - return printer.getMaxLengthEstimate(); - } - - // Basics - // ----------------------------------------------------------------------- - /** - *

Compares two objects for equality.

- * - * @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); - } - - /** - *

Returns a hash code compatible with equals.

- * - * @return a hash code compatible with equals - */ - @Override - public int hashCode() { - return printer.hashCode(); - } - - /** - *

Gets a debugging string version of this formatter.

- * - * @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 dff8e4a8702..00000000000 --- 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; - -/** - *

FastDatePrinter is a fast and thread-safe version of - * {@link java.text.SimpleDateFormat}.

- * - *

To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} - * or another variation of the factory methods of {@link FastDateFormat}.

- * - *

Since FastDatePrinter is thread safe, you can use a static member instance:

- * - * private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd"); - * - * - *

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. - *

- * - *

Only formatting is supported by this class, but all patterns are compatible with - * SimpleDateFormat (except time zones and some year patterns - see below).

- * - *

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).

- * - *

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.

- * - *

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. - * - *

Javadoc cites for the year pattern: For formatting, if the number of - * pattern letters is 2, the year is truncated to 2 digits; otherwise it is - * interpreted as a number. 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.

- * - *

- * Copied and modified from Apache Commons Lang. - *

- * - * @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 - // ----------------------------------------------------------------------- - /** - *

Constructs a new FastDatePrinter.

- * 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(); - } - - /** - *

Initializes the instance for first use.

- */ - private void init() { - final List 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 - // ----------------------------------------------------------------------- - /** - *

Returns a list of Rules given a pattern.

- * - * @return a {@code List} of Rule objects - * @throws IllegalArgumentException if pattern is invalid - */ - protected List parsePattern() { - final DateFormatSymbols symbols = new DateFormatSymbols(mLocale); - final List 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; - } - - /** - *

Performs the parsing of tokens.

- * - * @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(); - } - - /** - *

Gets an appropriate rule for the padding required.

- * - * @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 - // ----------------------------------------------------------------------- - /** - *

Formats a {@code Date}, {@code Calendar} or - * {@code Long} (milliseconds) object.

- * @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 ? "" : obj.getClass().getName())); - } - } - - /** - *

Formats a {@code Date}, {@code Calendar} or - * {@code Long} (milliseconds) object.

- * @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 ? "" : 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 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 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 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); - } - - /** - *

Performs the formatting by applying the rules to the - * specified calendar.

- * - * @param calendar the calendar to format - * @param buf the buffer to format into - * @param the Appendable class type, usually StringBuilder or StringBuffer. - * @return the specified string buffer - */ - private 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; - } - - /** - *

Gets an estimate for the maximum string length that the - * formatter will produce.

- * - *

The actual formatted length will almost always be less than or - * equal to this amount.

- * - * @return the maximum formatted length - */ - public int getMaxLengthEstimate() { - return mMaxLengthEstimate; - } - - // Basics - // ----------------------------------------------------------------------- - /** - *

Compares two objects for equality.

- * - * @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); - } - - /** - *

Returns a hash code compatible with equals.

- * - * @return a hash code compatible with equals - */ - @Override - public int hashCode() { - return mPattern.hashCode() + 13 * (mTimeZone.hashCode() + 13 * mLocale.hashCode()); - } - - /** - *

Gets a debugging string version of this formatter.

- * - * @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 - // ----------------------------------------------------------------------- - /** - *

Inner class defining a rule.

- */ - 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; - } - - /** - *

Inner class defining a numeric rule.

- */ - 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; - } - - /** - *

Inner class to output a constant single character.

- */ - 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); - } - } - - /** - *

Inner class to output a constant string.

- */ - 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); - } - } - - /** - *

Inner class to output one of a set of values.

- */ - 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)]); - } - } - - /** - *

Inner class to output an unpadded number.

- */ - 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); - } - } - } - - /** - *

Inner class to output an unpadded month.

- */ - 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); - } - } - } - - /** - *

Inner class to output a padded number.

- */ - 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); - } - } - - /** - *

Inner class to output a two digit number.

- */ - 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); - } - } - } - - /** - *

Inner class to output a two digit year.

- */ - 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); - } - } - - /** - *

Inner class to output a two digit month.

- */ - 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); - } - } - - /** - *

Inner class to output the twelve hour field.

- */ - 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); - } - } - - /** - *

Inner class to output the twenty four hour field.

- */ - 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); - } - } - - /** - *

Inner class to output the numeric day in week.

- */ - 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); - } - } - - /** - *

Inner class to output the numeric day in week.

- */ - 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 cTimeZoneDisplayCache = new ConcurrentHashMap<>(7); - /** - *

Gets the time zone display name, using a cache for performance.

- * - * @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; - } - - /** - *

Inner class to output a time zone name.

- */ - 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)); - } - } - } - - /** - *

Inner class to output a time zone as a number {@code +/-HHMM} - * or {@code +/-HH:MM}.

- */ - 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); - } - } - - /** - *

Inner class to output a time zone as a number {@code +/-HHMM} - * or {@code +/-HH:MM}.

- */ - 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); - } - } - - // ---------------------------------------------------------------------- - /** - *

Inner class that acts as a compound key for time zone names.

- */ - 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 b2986151b7b..00000000000 --- 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}. - *

- * 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 - *

- */ -@ProviderType -public class FixedDateFormat { - - /** - * Enumeration over the supported date/time format patterns. - *

- * Package protected for unit tests. - *

- */ - 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 DateTimeFormatter. - * - * @see DateTimeFormatter - */ - 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. - *

- * Package protected for unit tests. - *

- * - * @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. - *

- * Package protected for unit tests. - *

- * - * @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; - } - - /** - *

Returns the number of milliseconds since midnight in the time zone that this {@code FixedDateFormat} - * was constructed with for the specified currentTime.

- *

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.

- * @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 may 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/FormatCache.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FormatCache.java deleted file mode 100644 index 303255c6635..00000000000 --- 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; - -/** - *

FormatCache is a cache and factory for {@link Format}s.

- * - *

- * Copied and modified from Apache Commons Lang. - *

- * - * @since Apache Commons Lang 3.0 - */ -// TODO: Before making public move from getDateTimeInstance(Integer,...) to int; or some other approach. -abstract class FormatCache { - - /** - * No date or no time. Used in same parameters as DateFormat.SHORT or DateFormat.LONG - */ - static final int NONE = -1; - - private final ConcurrentMap cInstanceCache = new ConcurrentHashMap<>(7); - - private static final ConcurrentMap cDateTimeInstanceCache = new ConcurrentHashMap<>(7); - - /** - *

Gets a formatter instance using the default pattern in the - * default timezone and locale.

- * - * @return a date/time formatter - */ - public F getInstance() { - return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault()); - } - - /** - *

Gets a formatter instance using the specified pattern, time zone - * and locale.

- * - * @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 null - */ - 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; - } - - /** - *

Create a format instance using the specified pattern, time zone - * and locale.

- * - * @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 null - */ - protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale); - - /** - *

Gets a date/time formatter instance using the specified style, - * time zone and locale.

- * - * @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); - } - - /** - *

Gets a date/time formatter instance using the specified style, - * time zone and locale.

- * - * @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); - } - - /** - *

Gets a date formatter instance using the specified style, - * time zone and locale.

- * - * @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); - } - - /** - *

Gets a time formatter instance using the specified style, - * time zone and locale.

- * - * @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); - } - - /** - *

Gets a date/time format for the specified styles and locale.

- * - * @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; - } - - // ---------------------------------------------------------------------- - /** - *

Helper class to hold multi-part Map keys

- */ - private static class MultipartKey { - private final Object[] keys; - private int hashCode; - - /** - * Constructs an instance of MultipartKey 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 aec2664fa43..00000000000 --- 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 7214e443a87..00000000000 --- 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 { - - /** - *

Formats a millisecond {@code long} value.

- * - * @param millis the millisecond value to format - * @return the formatted string - * @since 2.1 - */ - String format(long millis); - - /** - *

Formats a {@code Date} object using a {@code GregorianCalendar}.

- * - * @param date the date to format - * @return the formatted string - */ - String format(Date date); - - /** - *

Formats a {@code Calendar} object.

- * 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); - - /** - *

Formats a millisecond {@code long} value into the - * supplied {@code Appendable}.

- * - * @param millis the millisecond value to format - * @param buf the buffer to format into - * @param the Appendable class type, usually StringBuilder or StringBuffer. - * @return the specified string buffer - * @since 3.5 - */ - B format(long millis, B buf); - - /** - *

Formats a {@code Date} object into the - * supplied {@code Appendable} using a {@code GregorianCalendar}.

- * - * @param date the date to format - * @param buf the buffer to format into - * @param the Appendable class type, usually StringBuilder or StringBuffer. - * @return the specified string buffer - * @since 3.5 - */ - B format(Date date, B buf); - - /** - *

Formats a {@code Calendar} object into the supplied {@code Appendable}.

- * 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 the Appendable class type, usually StringBuilder or StringBuffer. - * @return the specified string buffer - * @since 3.5 - */ - B format(Calendar calendar, B buf); - - // Accessors - // ----------------------------------------------------------------------- - /** - *

Gets the pattern used by this printer.

- * - * @return the pattern, {@link java.text.SimpleDateFormat} compatible - */ - String getPattern(); - - /** - *

Gets the time zone used by this printer.

- * - *

This zone is always used for {@code Date} printing.

- * - * @return the time zone - */ - TimeZone getTimeZone(); - - /** - *

Gets the locale used by this printer.

- * - * @return the locale - */ - Locale getLocale(); - - /** - *

Formats a {@code Date}, {@code Calendar} or - * {@code Long} (milliseconds) object.

- * - * @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 b15fd704dde..00000000000 --- 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 format(final long millis, final B buf) { - return formatter.format(millis, buf); - } - - @Override - public B format(final Date date, final B buf) { - return formatter.format(date, buf); - } - - @Override - public 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/datetime/Format.java deleted file mode 100644 index 38ea25e1f45..00000000000 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java +++ /dev/null @@ -1,32 +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; - -/** - * Use {@link org.apache.logging.log4j.core.time.internal.format.Format}. - */ -@Deprecated -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/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/time/internal/format/Format.java rename to log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantFormatter.java index dfecffbabe1..e53777813da 100644 --- 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/util/internal/instant/InstantFormatter.java @@ -14,18 +14,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.core.time.internal.format; +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; /** - * The basic methods for performing date formatting. + * Contract for formatting {@link Instant}s. + * + * @since 2.25.0 */ -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 00000000000..0ca4a982b31 --- /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 epoch1 seconds. + *

+ * 1 Epoch is a fixed instant on {@code 1970-01-01Z}. + *

+ *

Internal usage only!

+ *

+ * This class is intended only for internal Log4j usage. + * Log4j users should not use this class! + * This class is not subject to any backward compatibility concerns. + *

+ * + * @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 formatter; + + InstantNumberFormatter(final ChronoUnit precision, final BiConsumer 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 00000000000..9c93dd34066 --- /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. + *

+ * For instance, given the pattern {@code yyyy-MM-dd'T'HH:mm:ss.SSSX}, the generated formatter will + *

+ *
    + *
  1. Sequence the pattern and assign a time precision to each part (e.g., {@code MM} is of month precision)
  2. + *
  3. 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
  4. + *
  5. Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss.SSS})
  6. + *
+ *

Implementation note

+ *

+ * Formatting can actually even be made faster and garbage-free by manually formatting sub-minute precision directives as follows: + *

+ *
{@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();
+ * }
+ *

+ * 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. + *

+ * + * @since 2.25.0 + */ +final class InstantPatternDynamicFormatter implements InstantPatternFormatter { + + static final ChronoUnit PRECISION_THRESHOLD = ChronoUnit.MINUTES; + + private final AtomicReference 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 sequences = sequencePattern(pattern, precisionThreshold); + final List 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 sequencePattern(final String pattern, final ChronoUnit precisionThreshold) { + List sequences = sequencePattern(pattern); + final List mergedSequences = mergeDynamicSequences(sequences, precisionThreshold); + return mergeConsequentEffectivelyConstantSequences(mergedSequences, precisionThreshold); + } + + private static List sequencePattern(final String pattern) { + if (pattern.isEmpty()) { + return Collections.emptyList(); + } + final List 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. + * + *

+ * 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. + *

+ * + *

Example

+ * + *

+ * The {@code [MM-dd] HH:mm} pattern will result in following sequences: + *

+ * + *
{@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)
+     * ]
+     * }
+ * + *

+ * The above sequencing implies creation of 9 {@link AbstractFormatter}s. + * This method transforms it to the following: + *

+ * + *
{@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)
+     * ]
+     * }
+ * + *

+ * The above sequencing implies creation of 8 {@link AbstractFormatter}s. + *

+ * + * @param sequences sequences to be transformed + * @return transformed sequencing where consequent static sequences are merged + */ + private static List mergeConsequentStaticPatternSequences(final List sequences) { + + // Short-circuit if there is nothing to merge + if (sequences.size() < 2) { + return sequences; + } + + final List mergedSequences = new ArrayList<>(); + final List 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 mergedSequences, final List 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. + * + *

+ * 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}. + *

+ * + *

Example

+ * + *

+ * Assume {@link #mergeConsequentStaticPatternSequences(List)} produced the following: + *

+ * + *
{@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),
+     * ]
+     * }
+ * + *

+ * 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: + *

+ * + *
{@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),
+     * ]
+     * }
+ * + *

+ * 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}. + *

+ * + * @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 mergeDynamicSequences( + final List 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 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. + * + *

+ * 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. + *

+ * + *

Example

+ * + *

+ * Assume {@link #mergeDynamicSequences(List, ChronoUnit)} produced the following: + *

+ * + *
{@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),
+     * ]
+     * }
+ * + *

+ * The above sequencing implies creation of 12 {@link AbstractFormatter}s. + * This method transforms it to the following: + *

+ * + *
{@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),
+     * ]
+     * }
+ * + *

+ * The resultant sequencing effectively translates to 3 {@link AbstractFormatter}s. + *

+ * + * @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 mergeConsequentEffectivelyConstantSequences( + final List sequences, final ChronoUnit precisionThreshold) { + + // Short-circuit if there is nothing to merge + if (sequences.size() < 2) { + return sequences; + } + + final List mergedSequences = new ArrayList<>(); + boolean accumulatorConstant = true; + final List 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 mergedSequences, final List accumulatedSequences) { + mergeAccumulatedSequences( + mergedSequences, accumulatedSequences, () -> new CompositePatternSequence(accumulatedSequences)); + } + + private static void mergeAccumulatedSequences( + final List mergedSequences, + final List accumulatedSequences, + final Supplier 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 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 sequences) { + return sequences.stream() + .map(sequence -> sequence.precision) + .min(Comparator.comparing(ChronoUnit::getDuration)) + .get(); + } + + private static String concatSequencePatterns(List 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 00000000000..037e79a0097 --- /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. + *

Internal usage only!

+ *

+ * This class is intended only for internal Log4j usage. + * Log4j users should not use this class! + * This class is not subject to any backward compatibility concerns. + *

+ * + * @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 00000000000..96bf4504aa3 --- /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 epochInstantExtractor; + + private final ThreadLocal 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 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 f801bb4b4e7..3c87d12589e 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. + *

Internal usage only!

+ *

+ * This package is intended only for internal Log4j usage. + * Log4j users should not use this package! + * This package is not subject to any backward compatibility concerns. + *

+ * + * @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 21daa068190..bc9a36094da 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.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 createLiteLogEvents(final int logEventCount) { + public static List createLiteLogEvents(final int logEventCount) { + return createLiteLogEvents(logEventCount, TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT); + } + + public static List createLiteLogEvents( + final int logEventCount, final int timeOverlappingConsecutiveEventCount) { final List 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 @@ private static LogEvent createLiteLogEvent(final String id, final long timeMilli .build(); } - static List createFullLogEvents(final int logEventCount) { + public static List createFullLogEvents(final int logEventCount) { + return createFullLogEvents(logEventCount, TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT); + } + + public static List createFullLogEvents( + final int logEventCount, final int timeOverlappingConsecutiveEventCount) { final List 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 d4d9924283c..00000000000 --- 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 a9d0870e2fa..f6bc1050577 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.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 @@ * rounded = "rounded" -> boolean * * - * If no configuration options are provided, pattern-config is - * employed. There {@link - * JsonTemplateLayoutProperties#timestampFormatPattern()}, {@link - * JsonTemplateLayoutProperties#timeZone()}, {@link - * JsonTemplateLayoutProperties#locale()} are used as defaults for - * pattern, timeZone, and locale, respectively. + *

+ * If no configuration options are provided, pattern-config is employed. + * There {@link JsonTemplateLayoutProperties#timestampFormatPattern()}, {@link JsonTemplateLayoutProperties#timeZone()}, {@link JsonTemplateLayoutProperties#locale()} are used as defaults for pattern, timeZone, and locale, respectively. + *

* - * In epoch-config, millis.nanos, secs.nanos stand - * for the fractional component in nanoseconds. + *

+ * In epoch-config, millis.nanos, secs.nanos stand for the fractional component in nanoseconds. + *

* *

Examples

* @@ -207,100 +207,55 @@ private static EventResolver createResolver(final TemplateResolverConfig config) 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 @@ private static EventResolver createEpochResolver(final TemplateResolverConfig co 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 8f7522bb77e..00000000000 --- 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}. - *

- * 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. - *

- * 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 e22ffa31c74..00000000000 --- 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 threadLocalSDFormat = new ThreadLocal() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat("HH:mm:ss.SSS"); - } - }; - - private final ThreadLocal threadLocalCachedSDFormat = new ThreadLocal() { - @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 reusableBuffer = new ThreadLocal() { - @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 cachedTimeFastFmt = - new AtomicReference<>(new CachedTimeFastFormat(System.currentTimeMillis())); - private final AtomicReference 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 90d970f4bc9..00000000000 --- 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 threadLocalSimpleDateFormat = new ThreadLocal() { - @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 00000000000..d46c431ea0b --- /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 00000000000..0506318acd0 --- /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}. + *

+ * 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. + *

+ * + * @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 void validateInstants(final I[] instants) { + + // Find the instant offset + final Supplier 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 00000000000..53645ac7e6d --- /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 LITE_LOG_EVENTS = createLogEvents(LogEventFixture::createLiteLogEvents); + + private static final List FULL_LOG_EVENTS = createLogEvents(LogEventFixture::createFullLogEvents); + + private static List createLogEvents(final BiFunction> supplier) { + final int logEventCount = 1_000; + final List 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 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 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 00000000000..878e63cb017 --- /dev/null +++ b/src/changelog/.3.x.x/3149_remove_named_date_patterns.xml @@ -0,0 +1,8 @@ + + + + Remove _named_ date & time formatting patterns in Pattern Layout, except for `UNIX` and `UNIX_MILLIS` + diff --git a/src/changelog/.index.adoc.ftl b/src/changelog/.index.adoc.ftl index b71f9905f31..24c1ef38957 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 c7544624773..ef0a1c1c263 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]