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 &amp; 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 &amp; time patterns supported by Pattern 
Layout</description>
+</entry>

Reply via email to