Skip to content

Commit 2b1d469

Browse files
ashr123ppkarwaszRoy Ash
authored
Create NamedDatePattern to make date & time patterns supported by Pattern Layout programmatically accessible (#3789)
Co-authored-by: Piotr P. Karwasz <[email protected]> Co-authored-by: Roy Ash <[email protected]>
1 parent b6b1deb commit 2b1d469

File tree

6 files changed

+231
-105
lines changed

6 files changed

+231
-105
lines changed

log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Calendar;
2727
import java.util.Date;
2828
import java.util.TimeZone;
29+
import java.util.stream.Stream;
2930
import org.apache.logging.log4j.core.AbstractLogEvent;
3031
import org.apache.logging.log4j.core.LogEvent;
3132
import org.apache.logging.log4j.core.time.Instant;
@@ -329,29 +330,8 @@ void testNewInstanceAllowsNullParameter() {
329330
DatePatternConverter.newInstance(null); // no errors
330331
}
331332

332-
private static final String[] PATTERN_NAMES = {
333-
"ABSOLUTE",
334-
"ABSOLUTE_MICROS",
335-
"ABSOLUTE_NANOS",
336-
"ABSOLUTE_PERIOD",
337-
"COMPACT",
338-
"DATE",
339-
"DATE_PERIOD",
340-
"DEFAULT",
341-
"DEFAULT_MICROS",
342-
"DEFAULT_NANOS",
343-
"DEFAULT_PERIOD",
344-
"ISO8601_BASIC",
345-
"ISO8601_BASIC_PERIOD",
346-
"ISO8601",
347-
"ISO8601_OFFSET_DATE_TIME_HH",
348-
"ISO8601_OFFSET_DATE_TIME_HHMM",
349-
"ISO8601_OFFSET_DATE_TIME_HHCMM",
350-
"ISO8601_PERIOD",
351-
"ISO8601_PERIOD_MICROS",
352-
"US_MONTH_DAY_YEAR2_TIME",
353-
"US_MONTH_DAY_YEAR4_TIME"
354-
};
333+
private static final String[] PATTERN_NAMES =
334+
Stream.of(NamedInstantPattern.values()).map(Enum::name).toArray(String[]::new);
355335

356336
@Test
357337
void testPredefinedFormatWithoutTimezone() {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.core.pattern;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.time.Instant;
22+
import org.apache.logging.log4j.core.time.MutableInstant;
23+
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.EnumSource;
26+
27+
class NamedInstantPatternTest {
28+
29+
@ParameterizedTest
30+
@EnumSource(NamedInstantPattern.class)
31+
void compatibilityOfLegacyPattern(NamedInstantPattern namedPattern) {
32+
InstantPatternFormatter legacyFormatter = InstantPatternFormatter.newBuilder()
33+
.setPattern(namedPattern.getLegacyPattern())
34+
.setLegacyFormattersEnabled(true)
35+
.build();
36+
InstantPatternFormatter formatter = InstantPatternFormatter.newBuilder()
37+
.setPattern(namedPattern.getPattern())
38+
.setLegacyFormattersEnabled(false)
39+
.build();
40+
Instant javaTimeInstant = Instant.now();
41+
MutableInstant instant = new MutableInstant();
42+
instant.initFromEpochSecond(javaTimeInstant.getEpochSecond(), javaTimeInstant.getNano());
43+
assertThat(legacyFormatter.format(instant)).isEqualTo(formatter.format(instant));
44+
}
45+
}

log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java

Lines changed: 13 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ public final class DatePatternConverter extends LogEventPatternConverter impleme
4949

5050
private static final String CLASS_NAME = DatePatternConverter.class.getSimpleName();
5151

52-
private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS";
53-
5452
private final InstantFormatter formatter;
5553

5654
private DatePatternConverter(@Nullable final String[] options) {
@@ -64,7 +62,9 @@ private static InstantFormatter createFormatter(@Nullable final String[] options
6462
} catch (final Exception error) {
6563
logOptionReadFailure(options, error, "failed for options: {}, falling back to the default instance");
6664
}
67-
return InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build();
65+
return InstantPatternFormatter.newBuilder()
66+
.setPattern(NamedInstantPattern.DEFAULT.getPattern())
67+
.build();
6868
}
6969

7070
private static InstantFormatter createFormatterUnsafely(@Nullable final String[] options) {
@@ -94,7 +94,7 @@ private static InstantFormatter createFormatterUnsafely(@Nullable final String[]
9494
private static String readPattern(@Nullable final String[] options) {
9595
return options != null && options.length > 0 && options[0] != null
9696
? decodeNamedPattern(options[0])
97-
: DEFAULT_PATTERN;
97+
: NamedInstantPattern.DEFAULT.getPattern();
9898
}
9999

100100
/**
@@ -109,84 +109,16 @@ private static String readPattern(@Nullable final String[] options) {
109109
* @since 2.25.0
110110
*/
111111
static String decodeNamedPattern(final String pattern) {
112-
113-
// If legacy formatters are enabled, we need to produce output aimed for `FixedDateFormat` and `FastDateFormat`.
114-
// Otherwise, we need to produce output aimed for `DateTimeFormatter`.
115-
// In conclusion, we need to check if legacy formatters enabled and apply following transformations.
116-
//
117-
// | Microseconds | Nanoseconds | Time-zone
118-
// ------------------------------+--------------+-------------+-----------
119-
// Legacy formatter directive | nnnnnn | nnnnnnnnn | X, XX, XXX
120-
// `DateTimeFormatter` directive | SSSSSS | SSSSSSSSS | x, xx, xxx
121-
//
122-
// Enabling legacy formatters mean that user requests the pattern to be formatted using deprecated
123-
// `FixedDateFormat` and `FastDateFormat`.
124-
// These two have, let's not say _bogus_, but an _interesting_ way of handling certain pattern directives:
125-
//
126-
// - They say they adhere to `SimpleDateFormat` specification, but use `n` directive.
127-
// `n` is neither defined by `SimpleDateFormat`, nor `SimpleDateFormat` supports sub-millisecond precisions.
128-
// `n` is probably manually introduced by Log4j to support sub-millisecond precisions.
129-
//
130-
// - `n` denotes nano-of-second for `DateTimeFormatter`.
131-
// In Java 17, `n` and `N` (nano-of-day) always output nanosecond precision.
132-
// This is independent of how many times they occur consequently.
133-
// Yet legacy formatters use repeated `n` to denote sub-milliseconds precision of certain length.
134-
// This doesn't work for `DateTimeFormatter`, which needs
135-
//
136-
// - `SSSSSS` for 6-digit microsecond precision
137-
// - `SSSSSSSSS` for 9-digit nanosecond precision
138-
//
139-
// - Legacy formatters use `X`, `XX,` and `XXX` to choose between `+00`, `+0000`, or `+00:00`.
140-
// This is the correct behaviour for `SimpleDateFormat`.
141-
// Though `X` in `DateTimeFormatter` produces `Z` for zero-offset.
142-
// To avoid the `Z` output, one needs to use `x` with `DateTimeFormatter`.
143-
final boolean compat = InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED;
144-
145-
switch (pattern) {
146-
case "ABSOLUTE":
147-
return "HH:mm:ss,SSS";
148-
case "ABSOLUTE_MICROS":
149-
return "HH:mm:ss," + (compat ? "nnnnnn" : "SSSSSS");
150-
case "ABSOLUTE_NANOS":
151-
return "HH:mm:ss," + (compat ? "nnnnnnnnn" : "SSSSSSSSS");
152-
case "ABSOLUTE_PERIOD":
153-
return "HH:mm:ss.SSS";
154-
case "COMPACT":
155-
return "yyyyMMddHHmmssSSS";
156-
case "DATE":
157-
return "dd MMM yyyy HH:mm:ss,SSS";
158-
case "DATE_PERIOD":
159-
return "dd MMM yyyy HH:mm:ss.SSS";
160-
case "DEFAULT":
161-
return "yyyy-MM-dd HH:mm:ss,SSS";
162-
case "DEFAULT_MICROS":
163-
return "yyyy-MM-dd HH:mm:ss," + (compat ? "nnnnnn" : "SSSSSS");
164-
case "DEFAULT_NANOS":
165-
return "yyyy-MM-dd HH:mm:ss," + (compat ? "nnnnnnnnn" : "SSSSSSSSS");
166-
case "DEFAULT_PERIOD":
167-
return "yyyy-MM-dd HH:mm:ss.SSS";
168-
case "ISO8601_BASIC":
169-
return "yyyyMMdd'T'HHmmss,SSS";
170-
case "ISO8601_BASIC_PERIOD":
171-
return "yyyyMMdd'T'HHmmss.SSS";
172-
case "ISO8601":
173-
return "yyyy-MM-dd'T'HH:mm:ss,SSS";
174-
case "ISO8601_OFFSET_DATE_TIME_HH":
175-
return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "X" : "x");
176-
case "ISO8601_OFFSET_DATE_TIME_HHMM":
177-
return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "XX" : "xx");
178-
case "ISO8601_OFFSET_DATE_TIME_HHCMM":
179-
return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "XXX" : "xxx");
180-
case "ISO8601_PERIOD":
181-
return "yyyy-MM-dd'T'HH:mm:ss.SSS";
182-
case "ISO8601_PERIOD_MICROS":
183-
return "yyyy-MM-dd'T'HH:mm:ss." + (compat ? "nnnnnn" : "SSSSSS");
184-
case "US_MONTH_DAY_YEAR2_TIME":
185-
return "dd/MM/yy HH:mm:ss.SSS";
186-
case "US_MONTH_DAY_YEAR4_TIME":
187-
return "dd/MM/yyyy HH:mm:ss.SSS";
112+
// See `NamedInstantPattern.getLegacyPattern()`
113+
// for the difference between legacy and `DateTimeFormatter` patterns.
114+
try {
115+
NamedInstantPattern namedInstantPattern = NamedInstantPattern.valueOf(pattern);
116+
return InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED
117+
? namedInstantPattern.getLegacyPattern()
118+
: namedInstantPattern.getPattern();
119+
} catch (IllegalArgumentException ignored) {
120+
return pattern;
188121
}
189-
return pattern;
190122
}
191123

192124
private static TimeZone readTimeZone(@Nullable final String[] options) {
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.core.pattern;
18+
19+
import org.jspecify.annotations.NullMarked;
20+
21+
/**
22+
* Represents named date &amp; time patterns for formatting log timestamps.
23+
*
24+
* @see DatePatternConverter
25+
* @since 2.26.0
26+
*/
27+
@NullMarked
28+
public enum NamedInstantPattern {
29+
ABSOLUTE("HH:mm:ss,SSS"),
30+
31+
ABSOLUTE_MICROS("HH:mm:ss,SSSSSS", "HH:mm:ss,nnnnnn"),
32+
33+
ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS", "HH:mm:ss,nnnnnnnnn"),
34+
35+
ABSOLUTE_PERIOD("HH:mm:ss.SSS"),
36+
37+
COMPACT("yyyyMMddHHmmssSSS"),
38+
39+
DATE("dd MMM yyyy HH:mm:ss,SSS"),
40+
41+
DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS"),
42+
43+
DEFAULT("yyyy-MM-dd HH:mm:ss,SSS"),
44+
45+
DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnn"),
46+
47+
DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"),
48+
49+
DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS"),
50+
51+
ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS"),
52+
53+
ISO8601_BASIC_PERIOD("yyyyMMdd'T'HHmmss.SSS"),
54+
55+
ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS"),
56+
57+
ISO8601_OFFSET_DATE_TIME_HH("yyyy-MM-dd'T'HH:mm:ss,SSSx", "yyyy-MM-dd'T'HH:mm:ss,SSSX"),
58+
59+
ISO8601_OFFSET_DATE_TIME_HHMM("yyyy-MM-dd'T'HH:mm:ss,SSSxx", "yyyy-MM-dd'T'HH:mm:ss,SSSXX"),
60+
61+
ISO8601_OFFSET_DATE_TIME_HHCMM("yyyy-MM-dd'T'HH:mm:ss,SSSxxx", "yyyy-MM-dd'T'HH:mm:ss,SSSXXX"),
62+
63+
ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS"),
64+
65+
ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", "yyyy-MM-dd'T'HH:mm:ss.nnnnnn"),
66+
67+
US_MONTH_DAY_YEAR2_TIME("dd/MM/yy HH:mm:ss.SSS"),
68+
69+
US_MONTH_DAY_YEAR4_TIME("dd/MM/yyyy HH:mm:ss.SSS");
70+
71+
private final String pattern;
72+
private final String legacyPattern;
73+
74+
NamedInstantPattern(String pattern) {
75+
this(pattern, pattern);
76+
}
77+
78+
NamedInstantPattern(String pattern, String legacyPattern) {
79+
this.pattern = pattern;
80+
this.legacyPattern = legacyPattern;
81+
}
82+
83+
/**
84+
* Returns the date-time pattern string compatible with {@link java.time.format.DateTimeFormatter}
85+
* that is associated with this named pattern.
86+
*
87+
* @return the date-time pattern string for use with {@code DateTimeFormatter}
88+
*/
89+
public String getPattern() {
90+
return pattern;
91+
}
92+
93+
/**
94+
* Returns the legacy {@link org.apache.logging.log4j.core.util.datetime.FixedDateFormat} pattern
95+
* associated with this named pattern.
96+
* <p>
97+
* If legacy formatters are enabled, output is produced for
98+
* {@code FixedDateFormat} and {@code FastDateFormat}. To convert the {@code DateTimeFormatter}
99+
* to its legacy counterpart, the following transformations need to be applied:
100+
* </p>
101+
* <table>
102+
* <caption>Pattern Differences</caption>
103+
* <thead>
104+
* <tr>
105+
* <th></th>
106+
* <th>Microseconds</th>
107+
* <th>Nanoseconds</th>
108+
* <th>Time-zone</th>
109+
* </tr>
110+
* </thead>
111+
* <tbody>
112+
* <tr>
113+
* <td>Legacy formatter directive</td>
114+
* <td><code>nnnnnn</code></td>
115+
* <td><code>nnnnnnnnn</code></td>
116+
* <td><code>X</code>, <code>XX</code>, <code>XXX</code></td>
117+
* </tr>
118+
* <tr>
119+
* <td>{@code DateTimeFormatter} directive</td>
120+
* <td><code>SSSSSS</code></td>
121+
* <td><code>SSSSSSSSS</code></td>
122+
* <td><code>x</code>, <code>xx</code>, <code>xxx</code></td>
123+
* </tr>
124+
* </tbody>
125+
* </table>
126+
* <h4>Rationale</h4>
127+
* <ul>
128+
* <li>
129+
* <p>
130+
* Legacy formatters are largely compatible with the {@code SimpleDateFormat} specification,
131+
* but introduce a custom {@code n} pattern letter, unique to Log4j, to represent sub-millisecond precision.
132+
* This {@code n} is not part of the standard {@code SimpleDateFormat}.
133+
* </p>
134+
* <p>
135+
* In legacy formatters, repeating {@code n} increases the precision, similar to how repeated {@code S}
136+
* is used for fractional seconds in {@code DateTimeFormatter}.
137+
* </p>
138+
* <p>
139+
* In contrast, {@code DateTimeFormatter} interprets {@code n} as nano-of-second.
140+
* In Java 17, both {@code n} and {@code N} always output nanosecond precision,
141+
* regardless of the number of pattern letters.
142+
* </p>
143+
* </li>
144+
* <li>
145+
* <p>
146+
* Legacy formatters use <code>X</code>, <code>XX</code>, and <code>XXX</code> to format time zones as
147+
* <code>+00</code>, <code>+0000</code>, or <code>+00:00</code>, following {@code SimpleDateFormat} conventions.
148+
* In contrast, {@code DateTimeFormatter} outputs <code>Z</code> for zero-offset when using <code>X</code>.
149+
* To ensure numeric output for zero-offset (e.g., <code>+00</code>),
150+
* we use <code>x</code>, <code>xx</code>, or <code>xxx</code> instead.
151+
* </p>
152+
* </li>
153+
* </ul>
154+
*
155+
* @return the legacy pattern string as used in
156+
* {@link org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedFormat}
157+
*/
158+
String getLegacyPattern() {
159+
return legacyPattern;
160+
}
161+
}

log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* Provides classes implementing format specifiers in conversion patterns.
1919
*/
2020
@Export
21-
@Version("2.24.1")
21+
@Version("2.26.0")
2222
package org.apache.logging.log4j.core.pattern;
2323

2424
import org.osgi.annotation.bundle.Export;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="https://logging.apache.org/xml/ns"
4+
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
5+
type="added">
6+
<issue id="3789" link="https://github.com/apache/logging-log4j2/pull/3789"/>
7+
<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>
8+
</entry>

0 commit comments

Comments
 (0)