diff --git a/cf-java-logging-support-log4j2/pom.xml b/cf-java-logging-support-log4j2/pom.xml
index b792c832..7f0892fa 100644
--- a/cf-java-logging-support-log4j2/pom.xml
+++ b/cf-java-logging-support-log4j2/pom.xml
@@ -48,6 +48,25 @@
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ ${build-helper-maven-plugin.version}
+
+
+ add-jmh-sources
+ generate-test-sources
+
+ add-test-source
+
+
+
+ src/jmh/java
+
+
+
+
+
org.apache.maven.plugins
maven-compiler-plugin
@@ -72,6 +91,7 @@
org.apache.maven.plugins
maven-surefire-plugin
+ ${surefire.plugin.version}
true
@@ -81,4 +101,38 @@
+
+
+
+ benchmark
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ ${exec.plugin.version}
+
+
+ run-benchmarks
+ verify
+
+ exec
+
+
+ ${java.home}/bin/java
+ test
+
+ -classpath
+
+ com.sap.hcp.cf.log4j2.benchmark.BenchmarkRunner
+
+
+
+
+
+
+
+
+
+
diff --git a/cf-java-logging-support-log4j2/src/test/java/com/sap/hcp/cf/log4j2/benchmark/BenchmarkRunner.java b/cf-java-logging-support-log4j2/src/jmh/java/com/sap/hcp/cf/log4j2/benchmark/BenchmarkRunner.java
similarity index 100%
rename from cf-java-logging-support-log4j2/src/test/java/com/sap/hcp/cf/log4j2/benchmark/BenchmarkRunner.java
rename to cf-java-logging-support-log4j2/src/jmh/java/com/sap/hcp/cf/log4j2/benchmark/BenchmarkRunner.java
diff --git a/cf-java-logging-support-log4j2/src/test/java/com/sap/hcp/cf/log4j2/benchmark/EncodingBenchmarks.java b/cf-java-logging-support-log4j2/src/jmh/java/com/sap/hcp/cf/log4j2/benchmark/EncodingBenchmarks.java
similarity index 93%
rename from cf-java-logging-support-log4j2/src/test/java/com/sap/hcp/cf/log4j2/benchmark/EncodingBenchmarks.java
rename to cf-java-logging-support-log4j2/src/jmh/java/com/sap/hcp/cf/log4j2/benchmark/EncodingBenchmarks.java
index e42c4b0e..3fb44c8e 100644
--- a/cf-java-logging-support-log4j2/src/test/java/com/sap/hcp/cf/log4j2/benchmark/EncodingBenchmarks.java
+++ b/cf-java-logging-support-log4j2/src/jmh/java/com/sap/hcp/cf/log4j2/benchmark/EncodingBenchmarks.java
@@ -15,8 +15,11 @@
import static com.sap.hcp.cf.logging.common.request.RequestRecordBuilder.requestRecord;
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
-@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
public class EncodingBenchmarks {
public static Logger LOG = LoggerFactory.getLogger(EncodingBenchmarks.class);
diff --git a/cf-java-logging-support-logback/pom.xml b/cf-java-logging-support-logback/pom.xml
index 7a6cc712..4ecf7d02 100644
--- a/cf-java-logging-support-logback/pom.xml
+++ b/cf-java-logging-support-logback/pom.xml
@@ -35,9 +35,29 @@
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ ${build-helper-maven-plugin.version}
+
+
+ add-jmh-sources
+ generate-test-sources
+
+ add-test-source
+
+
+
+ src/jmh/java
+
+
+
+
+
org.apache.maven.plugins
maven-surefire-plugin
+ ${surefire.plugin.version}
true
@@ -47,4 +67,38 @@
+
+
+
+ benchmark
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ ${exec.plugin.version}
+
+
+ run-benchmarks
+ verify
+
+ exec
+
+
+ ${java.home}/bin/java
+ test
+
+ -classpath
+
+ com.sap.hcp.cf.logback.benchmark.BenchmarkRunner
+
+
+
+
+
+
+
+
+
+
diff --git a/cf-java-logging-support-logback/src/test/java/com/sap/hcp/cf/logback/benchmark/BenchmarkRunner.java b/cf-java-logging-support-logback/src/jmh/java/com/sap/hcp/cf/logback/benchmark/BenchmarkRunner.java
similarity index 100%
rename from cf-java-logging-support-logback/src/test/java/com/sap/hcp/cf/logback/benchmark/BenchmarkRunner.java
rename to cf-java-logging-support-logback/src/jmh/java/com/sap/hcp/cf/logback/benchmark/BenchmarkRunner.java
diff --git a/cf-java-logging-support-logback/src/test/java/com/sap/hcp/cf/logback/benchmark/EncodingBenchmarks.java b/cf-java-logging-support-logback/src/jmh/java/com/sap/hcp/cf/logback/benchmark/EncodingBenchmarks.java
similarity index 96%
rename from cf-java-logging-support-logback/src/test/java/com/sap/hcp/cf/logback/benchmark/EncodingBenchmarks.java
rename to cf-java-logging-support-logback/src/jmh/java/com/sap/hcp/cf/logback/benchmark/EncodingBenchmarks.java
index 4f5f502b..7bbd2f3e 100644
--- a/cf-java-logging-support-logback/src/test/java/com/sap/hcp/cf/logback/benchmark/EncodingBenchmarks.java
+++ b/cf-java-logging-support-logback/src/jmh/java/com/sap/hcp/cf/logback/benchmark/EncodingBenchmarks.java
@@ -15,8 +15,11 @@
import static com.sap.hcp.cf.logging.common.request.RequestRecordBuilder.requestRecord;
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
public class EncodingBenchmarks {
public static Logger LOG = LoggerFactory.getLogger(EncodingBenchmarks.class);
diff --git a/cf-java-logging-support-servlet/pom.xml b/cf-java-logging-support-servlet/pom.xml
index f1817148..299a1278 100644
--- a/cf-java-logging-support-servlet/pom.xml
+++ b/cf-java-logging-support-servlet/pom.xml
@@ -15,7 +15,6 @@
3.5.0
- 3.3.0
3.1.0
@@ -74,4 +73,67 @@
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ ${build-helper-maven-plugin.version}
+
+
+ add-jmh-sources
+ generate-test-sources
+
+ add-test-source
+
+
+
+ src/jmh/java
+
+
+
+
+
+
+
+
+
+
+ benchmark
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ ${exec.plugin.version}
+
+
+ run-benchmarks
+ verify
+
+ exec
+
+
+ ${java.home}/bin/java
+ test
+
+ -classpath
+
+ com.sap.hcp.cf.logging.servlet.filter.benchmark.BenchmarkRunner
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cf-java-logging-support-servlet/src/jmh/java/com/sap/hcp/cf/logging/servlet/filter/benchmark/BenchmarkRunner.java b/cf-java-logging-support-servlet/src/jmh/java/com/sap/hcp/cf/logging/servlet/filter/benchmark/BenchmarkRunner.java
new file mode 100644
index 00000000..cf6f0026
--- /dev/null
+++ b/cf-java-logging-support-servlet/src/jmh/java/com/sap/hcp/cf/logging/servlet/filter/benchmark/BenchmarkRunner.java
@@ -0,0 +1,15 @@
+package com.sap.hcp.cf.logging.servlet.filter.benchmark;
+
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+public class BenchmarkRunner {
+
+ public static void main(String[] args) throws RunnerException {
+ Options options =
+ new OptionsBuilder().include(RequestUriMatcherBenchmarks.class.getSimpleName()).forks(1).build();
+ new Runner(options).run();
+ }
+}
diff --git a/cf-java-logging-support-servlet/src/jmh/java/com/sap/hcp/cf/logging/servlet/filter/benchmark/RequestUriMatcherBenchmarks.java b/cf-java-logging-support-servlet/src/jmh/java/com/sap/hcp/cf/logging/servlet/filter/benchmark/RequestUriMatcherBenchmarks.java
new file mode 100644
index 00000000..8b3d0939
--- /dev/null
+++ b/cf-java-logging-support-servlet/src/jmh/java/com/sap/hcp/cf/logging/servlet/filter/benchmark/RequestUriMatcherBenchmarks.java
@@ -0,0 +1,86 @@
+package com.sap.hcp.cf.logging.servlet.filter.benchmark;
+
+import com.sap.hcp.cf.logging.servlet.filter.RequestUriMatcher;
+import org.openjdk.jmh.annotations.*;
+
+import java.util.concurrent.TimeUnit;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
+public class RequestUriMatcherBenchmarks {
+
+ @State(Scope.Benchmark)
+ public static class BenchmarkState {
+
+ // --- matchers ---
+ public final RequestUriMatcher noPatterns = new RequestUriMatcher(null);
+ public final RequestUriMatcher exactSingle = new RequestUriMatcher("/health");
+ public final RequestUriMatcher wildcardDouble = new RequestUriMatcher("/actuator/**");
+ public final RequestUriMatcher wildcardSingle = new RequestUriMatcher("/api/*/status");
+ public final RequestUriMatcher multiPattern =
+ new RequestUriMatcher("/health, /metrics, /actuator/**, /readyz, /livez");
+
+ // --- URIs ---
+ public final String uriHealth = "/health";
+ public final String uriApiOrders = "/api/orders";
+ public final String uriActuatorDeep = "/actuator/health/liveness";
+ public final String uriApiStatus = "/api/orders/status";
+ public final String uriNoMatch = "/api/v1/orders/123/items";
+ }
+
+ /** Baseline: no patterns configured — fastest possible path. */
+ @Benchmark
+ public boolean noPatterns(BenchmarkState s) {
+ return s.noPatterns.matches(s.uriApiOrders);
+ }
+
+ /** Single exact pattern, URI matches. */
+ @Benchmark
+ public boolean exactMatch(BenchmarkState s) {
+ return s.exactSingle.matches(s.uriHealth);
+ }
+
+ /** Single exact pattern, URI does not match. */
+ @Benchmark
+ public boolean exactNoMatch(BenchmarkState s) {
+ return s.exactSingle.matches(s.uriApiOrders);
+ }
+
+ /** Double-wildcard pattern (/actuator/**), deep URI matches. */
+ @Benchmark
+ public boolean doubleWildcardMatch(BenchmarkState s) {
+ return s.wildcardDouble.matches(s.uriActuatorDeep);
+ }
+
+ /** Double-wildcard pattern (/actuator/**), URI does not match. */
+ @Benchmark
+ public boolean doubleWildcardNoMatch(BenchmarkState s) {
+ return s.wildcardDouble.matches(s.uriNoMatch);
+ }
+
+ /** Single-segment wildcard ({@code /api/*/status}), URI matches. */
+ @Benchmark
+ public boolean singleWildcardMatch(BenchmarkState s) {
+ return s.wildcardSingle.matches(s.uriApiStatus);
+ }
+
+ /**
+ * Realistic actuator-exclusion scenario: five patterns, URI matches the third one (/actuator/**) — worst-case
+ * traversal through the list.
+ */
+ @Benchmark
+ public boolean multiPatternMatch(BenchmarkState s) {
+ return s.multiPattern.matches(s.uriActuatorDeep);
+ }
+
+ /**
+ * Realistic actuator-exclusion scenario: five patterns, URI matches none — full list traversal.
+ */
+ @Benchmark
+ public boolean multiPatternNoMatch(BenchmarkState s) {
+ return s.multiPattern.matches(s.uriNoMatch);
+ }
+}
diff --git a/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilter.java b/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilter.java
index 8c6edb49..63e8e1ad 100644
--- a/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilter.java
+++ b/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilter.java
@@ -23,11 +23,13 @@ public class GenerateRequestLogFilter extends AbstractLoggingFilter {
public static final String WRAP_RESPONSE_INIT_PARAM = "wrapResponse";
public static final String WRAP_REQUEST_INIT_PARAM = "wrapRequest";
+ public static final String EXCLUDE_PATTERNS_INIT_PARAM = "excludePatterns";
private final RequestRecordFactory requestRecordFactory;
private boolean wrapResponse = true;
private boolean wrapRequest = true;
+ private RequestUriMatcher excludeMatcher = new RequestUriMatcher(null);
public GenerateRequestLogFilter() {
this(new RequestRecordFactory(new LogOptionalFieldsSettings(GenerateRequestLogFilter.class.getName())));
@@ -47,6 +49,7 @@ public void init(FilterConfig filterConfig) throws ServletException {
if ("false".equalsIgnoreCase(value)) {
wrapRequest = false;
}
+ excludeMatcher = new RequestUriMatcher(filterConfig.getInitParameter(EXCLUDE_PATTERNS_INIT_PARAM));
}
@Override
@@ -75,7 +78,7 @@ protected void doFilterRequest(HttpServletRequest request, HttpServletResponse r
try {
doFilter(chain, request, response);
} finally {
- if (!request.isAsyncStarted()) {
+ if (!request.isAsyncStarted() && !excludeMatcher.matches(request.getRequestURI())) {
logger.logRequest();
}
diff --git a/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/RequestUriMatcher.java b/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/RequestUriMatcher.java
new file mode 100644
index 00000000..93da8d1e
--- /dev/null
+++ b/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/RequestUriMatcher.java
@@ -0,0 +1,99 @@
+package com.sap.hcp.cf.logging.servlet.filter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Matches request URIs against a list of Ant-style path patterns.
+ *
+ * Supported wildcards:
+ *
+ * - {@code *} – matches any sequence of characters within a single path segment (no slash)
+ * - {@code **} – matches any sequence of characters across path segment boundaries (including slashes)
+ *
+ */
+public class RequestUriMatcher {
+
+ private final List patterns;
+
+ /**
+ * Creates a matcher from a comma-separated list of Ant-style path patterns.
+ *
+ * @param commaSeparatedPatterns
+ * comma-separated pattern string, may be {@code null} or blank
+ */
+ public RequestUriMatcher(String commaSeparatedPatterns) {
+ if (commaSeparatedPatterns == null || commaSeparatedPatterns.isBlank()) {
+ this.patterns = Collections.emptyList();
+ } else {
+ this.patterns = Arrays.stream(commaSeparatedPatterns.split(",")).map(String::trim).filter(s -> !s.isEmpty())
+ .toList();
+ }
+ }
+
+ /**
+ * Recursively matches {@code pattern} starting at {@code pi} (pattern index) against {@code uri} starting at
+ * {@code ui} (uri index). No heap allocation occurs during matching.
+ */
+ private static boolean matches(String pattern, int pi, String uri, int ui) {
+ while (pi < pattern.length()) {
+ char pc = pattern.charAt(pi);
+ if (pc == '*') {
+ boolean doubleWildcard = pi + 1 < pattern.length() && pattern.charAt(pi + 1) == '*';
+ if (doubleWildcard) {
+ // ** — skip the ** and try matching the rest from every position in uri
+ pi += 2;
+ if (pi == pattern.length()) {
+ return true; // ** at end matches everything remaining
+ }
+ for (int i = ui; i <= uri.length(); i++) {
+ if (matches(pattern, pi, uri, i)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ // * — advance past * and try matching the rest from every position
+ // within the current segment (no slash crossing)
+ pi++;
+ if (pi == pattern.length()) {
+ return uri.indexOf('/', ui) == -1; // * at end matches rest of segment
+ }
+ for (int i = ui; i <= uri.length(); i++) {
+ if (i > ui && uri.charAt(i - 1) == '/') {
+ return false; // * cannot cross a slash
+ }
+ if (matches(pattern, pi, uri, i)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ } else {
+ // literal character — must match exactly
+ if (ui >= uri.length() || uri.charAt(ui) != pc) {
+ return false;
+ }
+ pi++;
+ ui++;
+ }
+ }
+ return ui == uri.length();
+ }
+
+ /**
+ * Returns {@code true} if the given URI matches any of the configured patterns.
+ */
+ public boolean matches(String uri) {
+ if (uri == null) {
+ return false;
+ }
+ for (String pattern: patterns) {
+ if (matches(pattern, 0, uri, 0)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilterTest.java b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilterTest.java
index 7645ed91..acc4aef3 100644
--- a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilterTest.java
+++ b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilterTest.java
@@ -119,10 +119,55 @@ public void directlyForwardsRequestResponseWhenLogIsDisabled(ConsoleOutput conso
assertThat(console.getAllEvents()).isEmpty();
((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(RequestLogger.class).setLevel(Level.INFO);
+ }
+
+ @Test
+ public void doesNotWriteRequestLogForExcludedUri(ConsoleOutput console) throws Exception {
+ when(request.getRequestURI()).thenReturn("/health");
+ GenerateRequestLogFilter filter = new GenerateRequestLogFilter(requestRecordFactory);
+ filter.init(new ExcludePatternsConfig("/health,/metrics"));
+
+ filter.doFilter(request, response, chain);
+
+ assertThat(console.getAllEvents()).isEmpty();
+ }
+
+ @Test
+ public void writesRequestLogForNonExcludedUri(ConsoleOutput console) throws Exception {
+ when(request.getRequestURI()).thenReturn("/api/orders");
+ GenerateRequestLogFilter filter = new GenerateRequestLogFilter(requestRecordFactory);
+ filter.init(new ExcludePatternsConfig("/health,/metrics"));
+
+ filter.doFilter(request, response, chain);
+
+ assertThat(console.getAllEvents()).isNotEmpty();
+ }
+
+ @Test
+ public void stillEnrichesContextForExcludedUri(ConsoleOutput console) throws Exception {
+ when(request.getRequestURI()).thenReturn("/actuator/health");
+ GenerateRequestLogFilter filter = new GenerateRequestLogFilter(requestRecordFactory);
+ filter.init(new ExcludePatternsConfig("/actuator/**"));
+
+ filter.doFilter(request, response, chain);
+
+ verify(request).setAttribute(eq(MDC.class.getName()), anyMap());
+ assertThat(console.getAllEvents()).isEmpty();
+ }
+
+ @Test
+ public void doesNotWriteRequestLogForWildcardExcludedUri(ConsoleOutput console) throws Exception {
+ when(request.getRequestURI()).thenReturn("/actuator/health/liveness");
+ GenerateRequestLogFilter filter = new GenerateRequestLogFilter(requestRecordFactory);
+ filter.init(new ExcludePatternsConfig("/actuator/**"));
+
+ filter.doFilter(request, response, chain);
+ assertThat(console.getAllEvents()).isEmpty();
}
private static class NoRequestWrappingConfig implements FilterConfig {
+
@Override
public String getFilterName() {
return "no-request-wrapping";
@@ -143,4 +188,36 @@ public Enumeration getInitParameterNames() {
return enumeration(List.of("wrapRequest"));
}
}
+
+ private static class ExcludePatternsConfig implements FilterConfig {
+
+ private final String excludePatterns;
+
+ ExcludePatternsConfig(String excludePatterns) {
+ this.excludePatterns = excludePatterns;
+ }
+
+ @Override
+ public String getFilterName() {
+ return "exclude-patterns";
+ }
+
+ @Override
+ public ServletContext getServletContext() {
+ return null;
+ }
+
+ @Override
+ public String getInitParameter(String name) {
+ if (GenerateRequestLogFilter.EXCLUDE_PATTERNS_INIT_PARAM.equals(name)) {
+ return excludePatterns;
+ }
+ return null;
+ }
+
+ @Override
+ public Enumeration getInitParameterNames() {
+ return enumeration(List.of(GenerateRequestLogFilter.EXCLUDE_PATTERNS_INIT_PARAM));
+ }
+ }
}
diff --git a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestUriMatcherTest.java b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestUriMatcherTest.java
new file mode 100644
index 00000000..c7cfcb79
--- /dev/null
+++ b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestUriMatcherTest.java
@@ -0,0 +1,99 @@
+package com.sap.hcp.cf.logging.servlet.filter;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RequestUriMatcherTest {
+
+ @Test
+ void doesNotMatchWhenNoPatternsConfigured() {
+ RequestUriMatcher matcher = new RequestUriMatcher(null);
+ assertThat(matcher.matches("/health")).isFalse();
+ }
+
+ @Test
+ void doesNotMatchWhenPatternStringIsBlank() {
+ RequestUriMatcher matcher = new RequestUriMatcher(" ");
+ assertThat(matcher.matches("/health")).isFalse();
+ }
+
+ @Test
+ void doesNotMatchNullUri() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/health");
+ assertThat(matcher.matches(null)).isFalse();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "/health", "/metrics", "/actuator" })
+ void matchesExactPattern(String uri) {
+ RequestUriMatcher matcher = new RequestUriMatcher("/health,/metrics,/actuator");
+ assertThat(matcher.matches(uri)).isTrue();
+ }
+
+ @Test
+ void doesNotMatchDifferentPath() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/health");
+ assertThat(matcher.matches("/api/orders")).isFalse();
+ }
+
+ @Test
+ void matchesSingleWildcardWithinSegment() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/api/*/status");
+ assertThat(matcher.matches("/api/orders/status")).isTrue();
+ }
+
+ @Test
+ void singleWildcardDoesNotMatchAcrossSegments() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/api/*/status");
+ assertThat(matcher.matches("/api/orders/items/status")).isFalse();
+ }
+
+ @ParameterizedTest
+ @CsvSource({ "/actuator/**, /actuator/health", "/actuator/**, /actuator/health/liveness",
+ "/actuator/**, /actuator/", "/api/**, /api/v1/orders/123" })
+ void matchesDoubleWildcardAcrossSegments(String pattern, String uri) {
+ RequestUriMatcher matcher = new RequestUriMatcher(pattern.trim());
+ assertThat(matcher.matches(uri.trim())).isTrue();
+ }
+
+ @Test
+ void doubleWildcardDoesNotMatchSiblingPath() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/actuator/**");
+ assertThat(matcher.matches("/api/orders")).isFalse();
+ }
+
+ @Test
+ void matchesFirstOfMultiplePatterns() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/health, /actuator/**, /metrics");
+ assertThat(matcher.matches("/health")).isTrue();
+ }
+
+ @Test
+ void matchesMiddleOfMultiplePatterns() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/health, /actuator/**, /metrics");
+ assertThat(matcher.matches("/actuator/health")).isTrue();
+ }
+
+ @Test
+ void matchesLastOfMultiplePatterns() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/health, /actuator/**, /metrics");
+ assertThat(matcher.matches("/metrics")).isTrue();
+ }
+
+ @Test
+ void doesNotMatchWhenNoneOfMultiplePatternsApply() {
+ RequestUriMatcher matcher = new RequestUriMatcher("/health, /actuator/**, /metrics");
+ assertThat(matcher.matches("/api/orders")).isFalse();
+ }
+
+ @Test
+ void ignoresWhitespaceAroundPatterns() {
+ RequestUriMatcher matcher = new RequestUriMatcher(" /health , /metrics ");
+ assertThat(matcher.matches("/health")).isTrue();
+ assertThat(matcher.matches("/metrics")).isTrue();
+ }
+}
diff --git a/pom.xml b/pom.xml
index a3917191..17515a86 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,6 +92,7 @@
3.5.0
3.5.3
3.6.3
+ 3.3.0
3.12.0
3.2.8
0.10.0