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 99bd9c706de..a01457ed6e4 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.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 @@ void testNewInstanceAllowsNullParameter() { 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(NamedDatePattern.values()).map(Enum::name).toArray(String[]::new); @Test void testPredefinedFormatWithoutTimezone() { 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 f26a6d54c56..90c1f589b19 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 @@ private static InstantFormatter createFormatter(@Nullable final String[] options } 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(NamedDatePattern.DEFAULT.getPattern()) + .build(); } private static InstantFormatter createFormatterUnsafely(@Nullable final String[] options) { @@ -94,7 +94,7 @@ private static InstantFormatter createFormatterUnsafely(@Nullable final String[] private static String readPattern(@Nullable final String[] options) { return options != null && options.length > 0 && options[0] != null ? decodeNamedPattern(options[0]) - : DEFAULT_PATTERN; + : NamedDatePattern.DEFAULT.getPattern(); } /** @@ -109,84 +109,11 @@ private static String readPattern(@Nullable final String[] options) { * @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"; + try { + return NamedDatePattern.valueOf(pattern).getPattern(); + } catch (IllegalArgumentException ignored) { // for Java 22+ it can be changed to `IllegalArgumentException _` + 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/NamedDatePattern.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedDatePattern.java new file mode 100644 index 00000000000..2eb87170e2a --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedDatePattern.java @@ -0,0 +1,92 @@ +/* + * 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.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter; + +// 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`. +/** + * Represents named date/time patterns for formatting log timestamps. + * Provides patterns for legacy and modern formatters based on configuration. + */ +@SuppressWarnings("SpellCheckingInspection") +public enum NamedDatePattern { + ABSOLUTE("HH:mm:ss,SSS"), + ABSOLUTE_MICROS("HH:mm:ss," + (InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED ? "nnnnnn" : "SSSSSS")), + ABSOLUTE_NANOS("HH:mm:ss," + (InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED ? "nnnnnnnnn" : "SSSSSSSSS")), + 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," + (InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED ? "nnnnnn" : "SSSSSS")), + DEFAULT_NANOS( + "yyyy-MM-dd HH:mm:ss," + (InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED ? "nnnnnnnnn" : "SSSSSSSSS")), + 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,SSS" + (InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED ? "X" : "x")), + ISO8601_OFFSET_DATE_TIME_HHMM( + "yyyy-MM-dd'T'HH:mm:ss,SSS" + (InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED ? "XX" : "xx")), + ISO8601_OFFSET_DATE_TIME_HHCMM( + "yyyy-MM-dd'T'HH:mm:ss,SSS" + (InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED ? "XXX" : "xxx")), + ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS"), + ISO8601_PERIOD_MICROS( + "yyyy-MM-dd'T'HH:mm:ss." + (InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED ? "nnnnnn" : "SSSSSS")), + 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; + + NamedDatePattern(String pattern) { + this.pattern = pattern; + } + + public String getPattern() { + return pattern; + } +} 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 ac6407f47b3..df5bc576a25 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 00000000000..377bf063178 --- /dev/null +++ b/src/changelog/.2.x.x/exported_named_patterns_into_public_enum.xml @@ -0,0 +1,7 @@ + + + Exported named-patterns into its own public enum +