Repository: helidon-io/helidon (java)

Issue:
PathMatcher fails to parse regex quantifier
## Environment Details
* Helidon Version: 4.2.0
* Helidon SE
* JDK version: N/A
* OS: N/A

----------

When using a custom regex pattern in a path, the pattern parser removes curly braces from regex quantifiers nested inside an expression.

Here is a simple unit test to demonstrate the problem

```java
import io.helidon.http.PathMatchers;
//...


@Test
public void testPathMatcher() throws Exception {
    final var matcher = PathMatchers.pattern("/{id:\\w{2}}/name");
    //Extract actual pattern - not pretty
    final var patternField = matcher.getClass().getDeclaredField("pattern");
    patternField.setAccessible(true);
    final var pattern = ((Pattern)patternField.get(matcher)).pattern();

    assertThat(pattern).isEqualTo("/(?<gfXdbHQlk0>\\w{2})/name");
}
```

Results in

> org.opentest4j.AssertionFailedError: 
> expected: "/(?<gfXdbHQlk0>\w{2})/name"
>  but was: "/(?<gfXdbHQlk0>\w2)/name"
> Expected :"/(?<gfXdbHQlk0>\w{2})/name"
> Actual   :"/(?<gfXdbHQlk0>\w2)/name"

i.e. `\\w{2}` becomes `\\w2`

---

I think the bug is in `io.helidon.http.PathMatchers#parseParamRegexp` where curly braces are counted and discarded - they should likely be added to the builder if `subSeqCounter > 0`

Pull request:
Update path matchers to honor quantifiers in Regular expressions
Resolves #9959 

### Description
No longer removes curly braces when processing regular expressions
Moved `PathMatcherTest` to the module that defines `PathMatcher`, it was left in the old place from previous version of Helidon.

### Documentation

None

Patch:
diff --git a/http/http/src/main/java/io/helidon/http/PathMatchers.java b/http/http/src/main/java/io/helidon/http/PathMatchers.java
index bbaaff18a8..9d8664b8c5 100644
--- a/http/http/src/main/java/io/helidon/http/PathMatchers.java
+++ b/http/http/src/main/java/io/helidon/http/PathMatchers.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -236,10 +236,14 @@ public final class PathMatchers {
             switch (ch) {
             case '{':
                 subSeqCounter++;
+                // nested curly braces should be added
+                builder.append('{');
                 break;
             case '}':
                 if (subSeqCounter > 0) {
                     subSeqCounter--;
+                    // nested curly braces should be added (except for the last one, because that is end of parameter)
+                    builder.append(ch);
                 } else {
                     return builder.toString();
                 }
@@ -452,6 +456,10 @@ public final class PathMatchers {
             return "pattern: " + patternString;
         }
 
+        Pattern pattern() {
+            return pattern;
+        }
+
         private Parameters extractParams(Matcher matcher) {
             // Old school, but slightly faster than stream.
             Map<String, String> params = new HashMap<>(paramToGroupName.size());


Test patch:
diff --git a/webserver/webserver/src/test/java/io/helidon/webserver/http/PathMatchersTest.java b/http/http/src/test/java/io/helidon/http/PathMatchersTest.java
similarity index 92%
rename from webserver/webserver/src/test/java/io/helidon/webserver/http/PathMatchersTest.java
rename to http/http/src/test/java/io/helidon/http/PathMatchersTest.java
index b37c7d4251..652ec8ffd3 100644
--- a/webserver/webserver/src/test/java/io/helidon/webserver/http/PathMatchersTest.java
+++ b/http/http/src/test/java/io/helidon/http/PathMatchersTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,14 +14,12 @@
  * limitations under the License.
  */
 
-package io.helidon.webserver.http;
+package io.helidon.http;
 
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import io.helidon.http.PathMatcher;
-import io.helidon.http.PathMatchers;
 import io.helidon.common.uri.UriPath;
 
 import org.hamcrest.Description;
@@ -35,7 +33,17 @@ import static org.hamcrest.MatcherAssert.assertThat;
 
 class PathMatchersTest {
     @Test
-    public void testNormalization() {
+    void testRegexQuantifier() {
+        var matcher = PathMatchers.pattern("/{id:\\w{2}}/name");
+        var patternMatcher = (PathMatchers.PatternPathMatcher) matcher;
+
+        var actualPattern = patternMatcher.pattern();
+
+        assertThat(actualPattern.pattern(), is("/(?<gfXdbHQlk0>\\w{2})/name"));
+    }
+
+    @Test
+    void testNormalization() {
         assertThat("/a/./b", pathMatcherMatches("/a/b"));
         assertThat("/a/b/../c", pathMatcherMatches("/a/c"));
     }
@@ -89,7 +97,7 @@ class PathMatchersTest {
     }
 
     @Test
-    public void testOptionals() {
+    void testOptionals() {
         assertThat("/foo/bar", pathMatcherMatches("/foo[/bar]"));
         assertThat("/foo", pathMatcherMatches("/foo[/bar]"));
         assertThat("/foo/ba", not(pathMatcherMatches("/foo[/bar]")));