This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/2.x by this push:
new 2b1d4692fa Create `NamedDatePattern` to make date & time patterns
supported by Pattern Layout programmatically accessible (#3789)
2b1d4692fa is described below
commit 2b1d4692faa03c3541e23cd194507bc3d2622bfa
Author: Roy <roy.ash...@gmail.com>
AuthorDate: Sun Jul 20 21:36:48 2025 +0300
Create `NamedDatePattern` to make date & time patterns supported by Pattern
Layout programmatically accessible (#3789)
Co-authored-by: Piotr P. Karwasz <pi...@github.copernik.eu>
Co-authored-by: Roy Ash <r...@getneema.com>
---
.../core/pattern/DatePatternConverterTestBase.java | 26 +---
.../core/pattern/NamedInstantPatternTest.java | 45 ++++++
.../log4j/core/pattern/DatePatternConverter.java | 94 ++----------
.../log4j/core/pattern/NamedInstantPattern.java | 161 +++++++++++++++++++++
.../logging/log4j/core/pattern/package-info.java | 2 +-
.../exported_named_patterns_into_public_enum.xml | 8 +
6 files changed, 231 insertions(+), 105 deletions(-)
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java
index 99bd9c706d..16fd89ac30 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java
@@ -26,6 +26,7 @@ import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
+import java.util.stream.Stream;
import org.apache.logging.log4j.core.AbstractLogEvent;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.time.Instant;
@@ -329,29 +330,8 @@ abstract class DatePatternConverterTestBase {
DatePatternConverter.newInstance(null); // no errors
}
- private static final String[] PATTERN_NAMES = {
- "ABSOLUTE",
- "ABSOLUTE_MICROS",
- "ABSOLUTE_NANOS",
- "ABSOLUTE_PERIOD",
- "COMPACT",
- "DATE",
- "DATE_PERIOD",
- "DEFAULT",
- "DEFAULT_MICROS",
- "DEFAULT_NANOS",
- "DEFAULT_PERIOD",
- "ISO8601_BASIC",
- "ISO8601_BASIC_PERIOD",
- "ISO8601",
- "ISO8601_OFFSET_DATE_TIME_HH",
- "ISO8601_OFFSET_DATE_TIME_HHMM",
- "ISO8601_OFFSET_DATE_TIME_HHCMM",
- "ISO8601_PERIOD",
- "ISO8601_PERIOD_MICROS",
- "US_MONTH_DAY_YEAR2_TIME",
- "US_MONTH_DAY_YEAR4_TIME"
- };
+ private static final String[] PATTERN_NAMES =
+
Stream.of(NamedInstantPattern.values()).map(Enum::name).toArray(String[]::new);
@Test
void testPredefinedFormatWithoutTimezone() {
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/NamedInstantPatternTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/NamedInstantPatternTest.java
new file mode 100644
index 0000000000..bbe5e6e45e
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/NamedInstantPatternTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.pattern;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import
org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+class NamedInstantPatternTest {
+
+ @ParameterizedTest
+ @EnumSource(NamedInstantPattern.class)
+ void compatibilityOfLegacyPattern(NamedInstantPattern namedPattern) {
+ InstantPatternFormatter legacyFormatter =
InstantPatternFormatter.newBuilder()
+ .setPattern(namedPattern.getLegacyPattern())
+ .setLegacyFormattersEnabled(true)
+ .build();
+ InstantPatternFormatter formatter =
InstantPatternFormatter.newBuilder()
+ .setPattern(namedPattern.getPattern())
+ .setLegacyFormattersEnabled(false)
+ .build();
+ Instant javaTimeInstant = Instant.now();
+ MutableInstant instant = new MutableInstant();
+ instant.initFromEpochSecond(javaTimeInstant.getEpochSecond(),
javaTimeInstant.getNano());
+
assertThat(legacyFormatter.format(instant)).isEqualTo(formatter.format(instant));
+ }
+}
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 f26a6d54c5..8b598c3de8 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
@@ -49,8 +49,6 @@ public final class DatePatternConverter extends
LogEventPatternConverter impleme
private static final String CLASS_NAME =
DatePatternConverter.class.getSimpleName();
- private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS";
-
private final InstantFormatter formatter;
private DatePatternConverter(@Nullable final String[] options) {
@@ -64,7 +62,9 @@ public final class DatePatternConverter extends
LogEventPatternConverter impleme
} catch (final Exception error) {
logOptionReadFailure(options, error, "failed for options: {},
falling back to the default instance");
}
- return
InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build();
+ return InstantPatternFormatter.newBuilder()
+ .setPattern(NamedInstantPattern.DEFAULT.getPattern())
+ .build();
}
private static InstantFormatter createFormatterUnsafely(@Nullable final
String[] options) {
@@ -94,7 +94,7 @@ public final class DatePatternConverter extends
LogEventPatternConverter impleme
private static String readPattern(@Nullable final String[] options) {
return options != null && options.length > 0 && options[0] != null
? decodeNamedPattern(options[0])
- : DEFAULT_PATTERN;
+ : NamedInstantPattern.DEFAULT.getPattern();
}
/**
@@ -109,84 +109,16 @@ public final class DatePatternConverter extends
LogEventPatternConverter impleme
* @since 2.25.0
*/
static String decodeNamedPattern(final String pattern) {
-
- // If legacy formatters are enabled, we need to produce output aimed
for `FixedDateFormat` and `FastDateFormat`.
- // Otherwise, we need to produce output aimed for `DateTimeFormatter`.
- // In conclusion, we need to check if legacy formatters enabled and
apply following transformations.
- //
- // | Microseconds | Nanoseconds |
Time-zone
- //
------------------------------+--------------+-------------+-----------
- // Legacy formatter directive | nnnnnn | nnnnnnnnn | X, XX,
XXX
- // `DateTimeFormatter` directive | SSSSSS | SSSSSSSSS | x, xx,
xxx
- //
- // Enabling legacy formatters mean that user requests the pattern to
be formatted using deprecated
- // `FixedDateFormat` and `FastDateFormat`.
- // These two have, let's not say _bogus_, but an _interesting_ way of
handling certain pattern directives:
- //
- // - They say they adhere to `SimpleDateFormat` specification, but use
`n` directive.
- // `n` is neither defined by `SimpleDateFormat`, nor
`SimpleDateFormat` supports sub-millisecond precisions.
- // `n` is probably manually introduced by Log4j to support
sub-millisecond precisions.
- //
- // - `n` denotes nano-of-second for `DateTimeFormatter`.
- // In Java 17, `n` and `N` (nano-of-day) always output nanosecond
precision.
- // This is independent of how many times they occur consequently.
- // Yet legacy formatters use repeated `n` to denote sub-milliseconds
precision of certain length.
- // This doesn't work for `DateTimeFormatter`, which needs
- //
- // - `SSSSSS` for 6-digit microsecond precision
- // - `SSSSSSSSS` for 9-digit nanosecond precision
- //
- // - Legacy formatters use `X`, `XX,` and `XXX` to choose between
`+00`, `+0000`, or `+00:00`.
- // This is the correct behaviour for `SimpleDateFormat`.
- // Though `X` in `DateTimeFormatter` produces `Z` for zero-offset.
- // To avoid the `Z` output, one needs to use `x` with
`DateTimeFormatter`.
- final boolean compat =
InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED;
-
- switch (pattern) {
- case "ABSOLUTE":
- return "HH:mm:ss,SSS";
- case "ABSOLUTE_MICROS":
- return "HH:mm:ss," + (compat ? "nnnnnn" : "SSSSSS");
- case "ABSOLUTE_NANOS":
- return "HH:mm:ss," + (compat ? "nnnnnnnnn" : "SSSSSSSSS");
- case "ABSOLUTE_PERIOD":
- return "HH:mm:ss.SSS";
- case "COMPACT":
- return "yyyyMMddHHmmssSSS";
- case "DATE":
- return "dd MMM yyyy HH:mm:ss,SSS";
- case "DATE_PERIOD":
- return "dd MMM yyyy HH:mm:ss.SSS";
- case "DEFAULT":
- return "yyyy-MM-dd HH:mm:ss,SSS";
- case "DEFAULT_MICROS":
- return "yyyy-MM-dd HH:mm:ss," + (compat ? "nnnnnn" : "SSSSSS");
- case "DEFAULT_NANOS":
- return "yyyy-MM-dd HH:mm:ss," + (compat ? "nnnnnnnnn" :
"SSSSSSSSS");
- case "DEFAULT_PERIOD":
- return "yyyy-MM-dd HH:mm:ss.SSS";
- case "ISO8601_BASIC":
- return "yyyyMMdd'T'HHmmss,SSS";
- case "ISO8601_BASIC_PERIOD":
- return "yyyyMMdd'T'HHmmss.SSS";
- case "ISO8601":
- return "yyyy-MM-dd'T'HH:mm:ss,SSS";
- case "ISO8601_OFFSET_DATE_TIME_HH":
- return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "X" : "x");
- case "ISO8601_OFFSET_DATE_TIME_HHMM":
- return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "XX" : "xx");
- case "ISO8601_OFFSET_DATE_TIME_HHCMM":
- return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "XXX" : "xxx");
- case "ISO8601_PERIOD":
- return "yyyy-MM-dd'T'HH:mm:ss.SSS";
- case "ISO8601_PERIOD_MICROS":
- return "yyyy-MM-dd'T'HH:mm:ss." + (compat ? "nnnnnn" :
"SSSSSS");
- case "US_MONTH_DAY_YEAR2_TIME":
- return "dd/MM/yy HH:mm:ss.SSS";
- case "US_MONTH_DAY_YEAR4_TIME":
- return "dd/MM/yyyy HH:mm:ss.SSS";
+ // See `NamedInstantPattern.getLegacyPattern()`
+ // for the difference between legacy and `DateTimeFormatter` patterns.
+ try {
+ NamedInstantPattern namedInstantPattern =
NamedInstantPattern.valueOf(pattern);
+ return InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED
+ ? namedInstantPattern.getLegacyPattern()
+ : namedInstantPattern.getPattern();
+ } catch (IllegalArgumentException ignored) {
+ return pattern;
}
- return pattern;
}
private static TimeZone readTimeZone(@Nullable final String[] options) {
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedInstantPattern.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedInstantPattern.java
new file mode 100644
index 0000000000..4ce82f15f8
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedInstantPattern.java
@@ -0,0 +1,161 @@
+/*
+ * 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.pattern;
+
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Represents named date & time patterns for formatting log timestamps.
+ *
+ * @see DatePatternConverter
+ * @since 2.26.0
+ */
+@NullMarked
+public enum NamedInstantPattern {
+ ABSOLUTE("HH:mm:ss,SSS"),
+
+ ABSOLUTE_MICROS("HH:mm:ss,SSSSSS", "HH:mm:ss,nnnnnn"),
+
+ ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS", "HH:mm:ss,nnnnnnnnn"),
+
+ ABSOLUTE_PERIOD("HH:mm:ss.SSS"),
+
+ COMPACT("yyyyMMddHHmmssSSS"),
+
+ DATE("dd MMM yyyy HH:mm:ss,SSS"),
+
+ DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS"),
+
+ DEFAULT("yyyy-MM-dd HH:mm:ss,SSS"),
+
+ DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnn"),
+
+ DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS", "yyyy-MM-dd
HH:mm:ss,nnnnnnnnn"),
+
+ DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS"),
+
+ ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS"),
+
+ ISO8601_BASIC_PERIOD("yyyyMMdd'T'HHmmss.SSS"),
+
+ ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS"),
+
+ ISO8601_OFFSET_DATE_TIME_HH("yyyy-MM-dd'T'HH:mm:ss,SSSx",
"yyyy-MM-dd'T'HH:mm:ss,SSSX"),
+
+ ISO8601_OFFSET_DATE_TIME_HHMM("yyyy-MM-dd'T'HH:mm:ss,SSSxx",
"yyyy-MM-dd'T'HH:mm:ss,SSSXX"),
+
+ ISO8601_OFFSET_DATE_TIME_HHCMM("yyyy-MM-dd'T'HH:mm:ss,SSSxxx",
"yyyy-MM-dd'T'HH:mm:ss,SSSXXX"),
+
+ ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS"),
+
+ ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
"yyyy-MM-dd'T'HH:mm:ss.nnnnnn"),
+
+ US_MONTH_DAY_YEAR2_TIME("dd/MM/yy HH:mm:ss.SSS"),
+
+ US_MONTH_DAY_YEAR4_TIME("dd/MM/yyyy HH:mm:ss.SSS");
+
+ private final String pattern;
+ private final String legacyPattern;
+
+ NamedInstantPattern(String pattern) {
+ this(pattern, pattern);
+ }
+
+ NamedInstantPattern(String pattern, String legacyPattern) {
+ this.pattern = pattern;
+ this.legacyPattern = legacyPattern;
+ }
+
+ /**
+ * Returns the date-time pattern string compatible with {@link
java.time.format.DateTimeFormatter}
+ * that is associated with this named pattern.
+ *
+ * @return the date-time pattern string for use with {@code
DateTimeFormatter}
+ */
+ public String getPattern() {
+ return pattern;
+ }
+
+ /**
+ * Returns the legacy {@link
org.apache.logging.log4j.core.util.datetime.FixedDateFormat} pattern
+ * associated with this named pattern.
+ * <p>
+ * If legacy formatters are enabled, output is produced for
+ * {@code FixedDateFormat} and {@code FastDateFormat}. To convert the
{@code DateTimeFormatter}
+ * to its legacy counterpart, the following transformations need to be
applied:
+ * </p>
+ * <table>
+ * <caption>Pattern Differences</caption>
+ * <thead>
+ * <tr>
+ * <th></th>
+ * <th>Microseconds</th>
+ * <th>Nanoseconds</th>
+ * <th>Time-zone</th>
+ * </tr>
+ * </thead>
+ * <tbody>
+ * <tr>
+ * <td>Legacy formatter directive</td>
+ * <td><code>nnnnnn</code></td>
+ * <td><code>nnnnnnnnn</code></td>
+ * <td><code>X</code>, <code>XX</code>, <code>XXX</code></td>
+ * </tr>
+ * <tr>
+ * <td>{@code DateTimeFormatter} directive</td>
+ * <td><code>SSSSSS</code></td>
+ * <td><code>SSSSSSSSS</code></td>
+ * <td><code>x</code>, <code>xx</code>, <code>xxx</code></td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <h4>Rationale</h4>
+ * <ul>
+ * <li>
+ * <p>
+ * Legacy formatters are largely compatible with the {@code
SimpleDateFormat} specification,
+ * but introduce a custom {@code n} pattern letter, unique to Log4j,
to represent sub-millisecond precision.
+ * This {@code n} is not part of the standard {@code
SimpleDateFormat}.
+ * </p>
+ * <p>
+ * In legacy formatters, repeating {@code n} increases the
precision, similar to how repeated {@code S}
+ * is used for fractional seconds in {@code DateTimeFormatter}.
+ * </p>
+ * <p>
+ * In contrast, {@code DateTimeFormatter} interprets {@code n} as
nano-of-second.
+ * In Java 17, both {@code n} and {@code N} always output nanosecond
precision,
+ * regardless of the number of pattern letters.
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Legacy formatters use <code>X</code>, <code>XX</code>, and
<code>XXX</code> to format time zones as
+ * <code>+00</code>, <code>+0000</code>, or <code>+00:00</code>,
following {@code SimpleDateFormat} conventions.
+ * In contrast, {@code DateTimeFormatter} outputs <code>Z</code> for
zero-offset when using <code>X</code>.
+ * To ensure numeric output for zero-offset (e.g., <code>+00</code>),
+ * we use <code>x</code>, <code>xx</code>, or <code>xxx</code>
instead.
+ * </p>
+ * </li>
+ * </ul>
+ *
+ * @return the legacy pattern string as used in
+ * {@link
org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedFormat}
+ */
+ String getLegacyPattern() {
+ return legacyPattern;
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
index ac6407f47b..df5bc576a2 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
@@ -18,7 +18,7 @@
* Provides classes implementing format specifiers in conversion patterns.
*/
@Export
-@Version("2.24.1")
+@Version("2.26.0")
package org.apache.logging.log4j.core.pattern;
import org.osgi.annotation.bundle.Export;
diff --git a/src/changelog/.2.x.x/exported_named_patterns_into_public_enum.xml
b/src/changelog/.2.x.x/exported_named_patterns_into_public_enum.xml
new file mode 100644
index 0000000000..20fd32265d
--- /dev/null
+++ b/src/changelog/.2.x.x/exported_named_patterns_into_public_enum.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="https://logging.apache.org/xml/ns"
+ xsi:schemaLocation="https://logging.apache.org/xml/ns
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="added">
+ <issue id="3789" link="https://github.com/apache/logging-log4j2/pull/3789"/>
+ <description format="asciidoc">Add and export
`org.apache.logging.log4j.core.pattern.NamedInstantPattern` enabling users to
programmatically access named date & time patterns supported by Pattern
Layout</description>
+</entry>