From 6a0529eb2caefabbade7ce22d81c204d1f9f6802 Mon Sep 17 00:00:00 2001 From: Ramanathan Date: Sun, 10 May 2026 00:18:46 +0530 Subject: [PATCH 1/2] Refactor RegexFilter to use a plugin builder, restore configurable regex patternFlags via a CSV attribute, and improve handling of useRawMsg with a primitive boolean. --- .../log4j/core/filter/RegexFilterTest.java | 16 +++ .../filter/RegexFilterPatternFlagsTest.xml | 25 ++++ .../log4j/core/filter/RegexFilter.java | 123 +++++++++++++----- .../log4j/core/filter/package-info.java | 2 +- .../regexfilter_patternflags_builder.xml | 11 ++ .../modules/ROOT/pages/manual/filters.adoc | 6 + 6 files changed, 149 insertions(+), 34 deletions(-) create mode 100644 log4j-core-test/src/test/resources/filter/RegexFilterPatternFlagsTest.xml create mode 100644 src/changelog/.2.x.x/regexfilter_patternflags_builder.xml diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/RegexFilterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/RegexFilterTest.java index 671d998258b..bf7cd93796f 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/RegexFilterTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/filter/RegexFilterTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -27,6 +28,9 @@ import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Filter.Result; import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Configurator; import org.apache.logging.log4j.core.impl.Log4jLogEvent; import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.message.SimpleMessage; @@ -91,6 +95,18 @@ void testNoMsg() throws Exception { assertSame(Filter.Result.DENY, filter.filter(null, Level.DEBUG, null, null, (Object[]) null)); } + @Test + void testPatternFlagsFromConfiguration() { + try (final LoggerContext context = + Configurator.initialize("RegexFilterPatternFlagsTest", "filter/RegexFilterPatternFlagsTest.xml")) { + final Configuration configuration = context.getConfiguration(); + final Filter filter = configuration.getFilter(); + assertNotNull(filter); + assertThat(filter.filter(null, null, null, (Object) "ERROR", null), equalTo(Result.ACCEPT)); + assertThat(filter.filter(null, null, null, (Object) "warn", null), equalTo(Result.DENY)); + } + } + @Test void testParameterizedMsg() throws Exception { final String msg = "params {} {}"; diff --git a/log4j-core-test/src/test/resources/filter/RegexFilterPatternFlagsTest.xml b/log4j-core-test/src/test/resources/filter/RegexFilterPatternFlagsTest.xml new file mode 100644 index 00000000000..66d79c8e0c8 --- /dev/null +++ b/log4j-core-test/src/test/resources/filter/RegexFilterPatternFlagsTest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/RegexFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/RegexFilter.java index 62d41b31f59..fba195831f3 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/RegexFilter.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/RegexFilter.java @@ -16,9 +16,7 @@ */ package org.apache.logging.log4j.core.filter; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Comparator; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.logging.log4j.Level; @@ -29,13 +27,15 @@ import org.apache.logging.log4j.core.config.Node; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginElement; -import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required; import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.message.MessageFormatMessage; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.message.StringFormattedMessage; import org.apache.logging.log4j.message.StructuredDataMessage; +import org.apache.logging.log4j.util.Strings; /** * A filter that matches the given regular expression pattern against messages. @@ -116,6 +116,11 @@ public String toString() { return sb.toString(); } + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + /** * Creates a Filter that matches a regular expression. * @@ -130,48 +135,100 @@ public String toString() { * @param mismatch * The action to perform when a mismatch occurs. * @return The RegexFilter. - * @throws IllegalAccessException When there is no access to the definition of the specified member. - * @throws IllegalArgumentException When passed an illegal or inappropriate argument. */ - // TODO Consider refactoring to use AbstractFilter.AbstractFilterBuilder - @PluginFactory public static RegexFilter createFilter( // @formatter:off @PluginAttribute("regex") final String regex, - @PluginElement("PatternFlags") final String[] patternFlags, + @PluginAttribute("patternFlags") final String[] patternFlags, @PluginAttribute("useRawMsg") final Boolean useRawMsg, @PluginAttribute("onMatch") final Result match, @PluginAttribute("onMismatch") final Result mismatch) - // @formatter:on - throws IllegalArgumentException, IllegalAccessException { - if (regex == null) { - LOGGER.error("A regular expression must be provided for RegexFilter"); - return null; + // @formatter:on + { + final String flags = patternFlags == null ? null : String.join(",", patternFlags); + return RegexFilter.newBuilder() + .setRegex(regex) + .setPatternFlags(flags) + .setUseRawMsg(Boolean.TRUE.equals(useRawMsg)) + .setOnMatch(match) + .setOnMismatch(mismatch) + .build(); + } + + private static int toPatternFlags(final String patternFlags) { + if (Strings.isBlank(patternFlags)) { + return DEFAULT_PATTERN_FLAGS; } - return new RegexFilter( - Boolean.TRUE.equals(useRawMsg), Pattern.compile(regex, toPatternFlags(patternFlags)), match, mismatch); + int flags = DEFAULT_PATTERN_FLAGS; + for (final String flagName : Strings.splitList(patternFlags)) { + flags |= toPatternFlag(flagName); + } + return flags; } - private static int toPatternFlags(final String[] patternFlags) - throws IllegalArgumentException, IllegalAccessException { - if (patternFlags == null || patternFlags.length == 0) { + private static int toPatternFlag(final String flagName) { + if (Strings.isBlank(flagName)) { return DEFAULT_PATTERN_FLAGS; } - final Field[] fields = Pattern.class.getDeclaredFields(); - final Comparator comparator = (f1, f2) -> f1.getName().compareTo(f2.getName()); - Arrays.sort(fields, comparator); - final String[] fieldNames = new String[fields.length]; - for (int i = 0; i < fields.length; i++) { - fieldNames[i] = fields[i].getName(); + switch (flagName.trim().toUpperCase(Locale.ROOT)) { + case "UNIX_LINES": + return Pattern.UNIX_LINES; + case "CASE_INSENSITIVE": + return Pattern.CASE_INSENSITIVE; + case "COMMENTS": + return Pattern.COMMENTS; + case "MULTILINE": + return Pattern.MULTILINE; + case "LITERAL": + return Pattern.LITERAL; + case "DOTALL": + return Pattern.DOTALL; + case "UNICODE_CASE": + return Pattern.UNICODE_CASE; + case "CANON_EQ": + return Pattern.CANON_EQ; + case "UNICODE_CHARACTER_CLASS": + return Pattern.UNICODE_CHARACTER_CLASS; + default: + return DEFAULT_PATTERN_FLAGS; } - int flags = DEFAULT_PATTERN_FLAGS; - for (final String test : patternFlags) { - final int index = Arrays.binarySearch(fieldNames, test); - if (index >= 0) { - final Field field = fields[index]; - flags |= field.getInt(Pattern.class); + } + + public static class Builder extends AbstractFilterBuilder + implements org.apache.logging.log4j.core.util.Builder { + + @PluginBuilderAttribute + @Required(message = "A regular expression must be provided for RegexFilter") + private String regex; + + @PluginBuilderAttribute + private String patternFlags; + + @PluginBuilderAttribute("useRawMsg") + private boolean useRawMsg; + + public Builder setRegex(final String regex) { + this.regex = regex; + return this; + } + + public Builder setPatternFlags(final String patternFlags) { + this.patternFlags = patternFlags; + return this; + } + + public Builder setUseRawMsg(final boolean useRawMsg) { + this.useRawMsg = useRawMsg; + return this; + } + + @Override + public RegexFilter build() { + if (!isValid()) { + return null; } + return new RegexFilter( + useRawMsg, Pattern.compile(regex, toPatternFlags(patternFlags)), getOnMatch(), getOnMismatch()); } - return flags; } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java index 38173ef4735..f02feaa580e 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java @@ -22,7 +22,7 @@ * {@link org.apache.logging.log4j.core.Filter#ELEMENT_TYPE filter}. */ @Export -@Version("2.25.3") +@Version("2.26.0") package org.apache.logging.log4j.core.filter; import org.osgi.annotation.bundle.Export; diff --git a/src/changelog/.2.x.x/regexfilter_patternflags_builder.xml b/src/changelog/.2.x.x/regexfilter_patternflags_builder.xml new file mode 100644 index 00000000000..cb39a526f27 --- /dev/null +++ b/src/changelog/.2.x.x/regexfilter_patternflags_builder.xml @@ -0,0 +1,11 @@ + + + + + Refactor `RegexFilter` to a plugin builder, restore configurable regex `patternFlags` via a CSV attribute, and avoid `useRawMsg` null-unboxing pitfalls by using a primitive boolean in the builder path. + + + diff --git a/src/site/antora/modules/ROOT/pages/manual/filters.adoc b/src/site/antora/modules/ROOT/pages/manual/filters.adoc index f5ecc6f6f36..fc1de7fd434 100644 --- a/src/site/antora/modules/ROOT/pages/manual/filters.adoc +++ b/src/site/antora/modules/ROOT/pages/manual/filters.adoc @@ -767,6 +767,12 @@ Besides the <>, | `boolean` | `false` | If `true`, for xref:manual/messages.adoc#ParameterizedMessage[`ParameterizedMessage`], xref:manual/messages.adoc#StringFormattedMessage[`StringFormattedMessage`], and xref:manual/messages.adoc#MessageFormatMessage[`MessageFormatMessage`], the message format pattern; for xref:manual/messages.adoc#StructuredDataMessage[`StructuredDataMessage`], the message field will be used as the match target. + +| patternFlags +| `String` +| +| Comma-separated list of https://docs.oracle.com/javase/{java-target-version}/docs/api/java/util/regex/Pattern.html[`Pattern`] flag names. +Supported values are `UNIX_LINES`, `CASE_INSENSITIVE`, `COMMENTS`, `MULTILINE`, `LITERAL`, `DOTALL`, `UNICODE_CASE`, `CANON_EQ`, and `UNICODE_CHARACTER_CLASS`. |=== [WARNING] From 16a78a64f24b1cbd73edaf9e5944c38bc1e0ecf3 Mon Sep 17 00:00:00 2001 From: Ramanathan Date: Sun, 10 May 2026 00:26:14 +0530 Subject: [PATCH 2/2] update pr url in change log --- src/changelog/.2.x.x/regexfilter_patternflags_builder.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/changelog/.2.x.x/regexfilter_patternflags_builder.xml b/src/changelog/.2.x.x/regexfilter_patternflags_builder.xml index cb39a526f27..8e4d97dc706 100644 --- a/src/changelog/.2.x.x/regexfilter_patternflags_builder.xml +++ b/src/changelog/.2.x.x/regexfilter_patternflags_builder.xml @@ -4,6 +4,7 @@ xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" type="fixed"> + Refactor `RegexFilter` to a plugin builder, restore configurable regex `patternFlags` via a CSV attribute, and avoid `useRawMsg` null-unboxing pitfalls by using a primitive boolean in the builder path.