Skip to content

Commit 9657563

Browse files
committed
fix: expose legacy FixedFormat patterns via NamedInstantPattern
To decouple `DatePatternConverter` from `FixedFormat`, legacy pattern values are now provided through the `NamedInstantPattern.getLegacyPattern` method. These patterns are specific to `FixedDateFormat` and not intended for general use, so the method is kept package-private.
1 parent 6b311cd commit 9657563

File tree

3 files changed

+87
-40
lines changed

3 files changed

+87
-40
lines changed
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: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -109,40 +109,19 @@ private static String readPattern(@Nullable final String[] options) {
109109
* @since 2.25.0
110110
*/
111111
static String decodeNamedPattern(final String pattern) {
112-
// If legacy formatters are enabled, we need to produce output aimed for `FixedDateFormat` and `FastDateFormat`.
113-
// Otherwise, we need to produce output aimed for `DateTimeFormatter`.
114-
// In conclusion, we need to check if legacy formatters enabled and apply following transformations.
112+
// `FixedDateFormat` accepted two types of patterns:
113+
// - the names of `FixedFormat` enum constants (identical to `NamedInstantPattern` enum names),
114+
// - or custom pattern strings.
115115
//
116-
// | Microseconds | Nanoseconds | Time-zone
117-
// ------------------------------+--------------+-------------+-----------
118-
// Legacy formatter directive | nnnnnn | nnnnnnnnn | X, XX, XXX
119-
// `DateTimeFormatter` directive | SSSSSS | SSSSSSSSS | x, xx, xxx
120-
//
121-
// Enabling legacy formatters mean that user requests the pattern to be formatted using deprecated
122-
// `FixedDateFormat` and `FastDateFormat`.
123-
// These two have, let's not say _bogus_, but an _interesting_ way of handling certain pattern directives:
124-
//
125-
// - They say they adhere to `SimpleDateFormat` specification, but use `n` directive.
126-
// `n` is neither defined by `SimpleDateFormat`, nor `SimpleDateFormat` supports sub-millisecond precisions.
127-
// `n` is probably manually introduced by Log4j to support sub-millisecond precisions.
128-
//
129-
// - `n` denotes nano-of-second for `DateTimeFormatter`.
130-
// In Java 17, `n` and `N` (nano-of-day) always output nanosecond precision.
131-
// This is independent of how many times they occur consequently.
132-
// Yet legacy formatters use repeated `n` to denote sub-milliseconds precision of certain length.
133-
// This doesn't work for `DateTimeFormatter`, which needs
134-
//
135-
// - `SSSSSS` for 6-digit microsecond precision
136-
// - `SSSSSSSSS` for 9-digit nanosecond precision
137-
//
138-
// - Legacy formatters use `X`, `XX,` and `XXX` to choose between `+00`, `+0000`, or `+00:00`.
139-
// This is the correct behaviour for `SimpleDateFormat`.
140-
// Though `X` in `DateTimeFormatter` produces `Z` for zero-offset.
141-
// To avoid the `Z` output, one needs to use `x` with `DateTimeFormatter`.
116+
// To determine the format's precision, we cannot return the legacy name directly;
117+
// instead, we must return the equivalent `FixedDateFormat` pattern string.
118+
// These patterns are only recognized by `FixedDateFormat` so we make them available only
119+
// via the package-private `getLegacyPattern()` method.
142120
try {
121+
NamedInstantPattern namedInstantPattern = NamedInstantPattern.valueOf(pattern);
143122
return InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED
144-
? FixedDateFormat.FixedFormat.valueOf(pattern).getPattern()
145-
: NamedInstantPattern.valueOf(pattern).getPattern();
123+
? namedInstantPattern.getLegacyPattern()
124+
: namedInstantPattern.getPattern();
146125
} catch (IllegalArgumentException ignored) {
147126
return pattern;
148127
}

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

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,21 @@
1616
*/
1717
package org.apache.logging.log4j.core.pattern;
1818

19+
import org.jspecify.annotations.NullMarked;
20+
1921
/**
2022
* Represents named date & time patterns for formatting log timestamps.
2123
*
2224
* @see DatePatternConverter
2325
* @since 2.26.0
2426
*/
27+
@NullMarked
2528
public enum NamedInstantPattern {
2629
ABSOLUTE("HH:mm:ss,SSS"),
2730

28-
ABSOLUTE_MICROS("HH:mm:ss,SSSSSS"),
31+
ABSOLUTE_MICROS("HH:mm:ss,SSSSSS", "HH:mm:ss,nnnnnn"),
2932

30-
ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS"),
33+
ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS", "HH:mm:ss,nnnnnnnnn"),
3134

3235
ABSOLUTE_PERIOD("HH:mm:ss.SSS"),
3336

@@ -39,9 +42,9 @@ public enum NamedInstantPattern {
3942

4043
DEFAULT("yyyy-MM-dd HH:mm:ss,SSS"),
4144

42-
DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS"),
45+
DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnn"),
4346

44-
DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS"),
47+
DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"),
4548

4649
DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS"),
4750

@@ -51,30 +54,50 @@ public enum NamedInstantPattern {
5154

5255
ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS"),
5356

54-
ISO8601_OFFSET_DATE_TIME_HH("yyyy-MM-dd'T'HH:mm:ss,SSSx"),
57+
ISO8601_OFFSET_DATE_TIME_HH("yyyy-MM-dd'T'HH:mm:ss,SSSx", "yyyy-MM-dd'T'HH:mm:ss,SSSX"),
5558

56-
ISO8601_OFFSET_DATE_TIME_HHMM("yyyy-MM-dd'T'HH:mm:ss,SSSxx"),
59+
ISO8601_OFFSET_DATE_TIME_HHMM("yyyy-MM-dd'T'HH:mm:ss,SSSxx", "yyyy-MM-dd'T'HH:mm:ss,SSSXX"),
5760

58-
ISO8601_OFFSET_DATE_TIME_HHCMM("yyyy-MM-dd'T'HH:mm:ss,SSSxxx"),
61+
ISO8601_OFFSET_DATE_TIME_HHCMM("yyyy-MM-dd'T'HH:mm:ss,SSSxxx", "yyyy-MM-dd'T'HH:mm:ss,SSSXXX"),
5962

6063
ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS"),
6164

62-
ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"),
65+
ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", "yyyy-MM-dd'T'HH:mm:ss.nnnnnn"),
6366

6467
US_MONTH_DAY_YEAR2_TIME("dd/MM/yy HH:mm:ss.SSS"),
6568

6669
US_MONTH_DAY_YEAR4_TIME("dd/MM/yyyy HH:mm:ss.SSS");
6770

6871
private final String pattern;
72+
private final String legacyPattern;
6973

7074
NamedInstantPattern(String pattern) {
75+
this(pattern, pattern);
76+
}
77+
78+
NamedInstantPattern(String pattern, String legacyPattern) {
7179
this.pattern = pattern;
80+
this.legacyPattern = legacyPattern;
7281
}
7382

7483
/**
75-
* @return pattern that is compatible with {@link java.time.format.DateTimeFormatter}
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}
7688
*/
7789
public String getPattern() {
7890
return pattern;
7991
}
92+
93+
/**
94+
* Returns the legacy {@link org.apache.logging.log4j.core.util.datetime.FixedDateFormat} pattern
95+
* associated with this named pattern.
96+
*
97+
* @return the legacy pattern string as used in
98+
* {@link org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedFormat}
99+
*/
100+
String getLegacyPattern() {
101+
return legacyPattern;
102+
}
80103
}

0 commit comments

Comments
 (0)