From 960d4490a359a518fcdbe9378a0b1ba2c9309960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Tollar?= Date: Mon, 21 Sep 2026 15:29:11 +0200 Subject: [PATCH] perf: compile ApiPathImpl part patterns once per instance partMatches compiled a Pattern on every call. Operation resolution compares a request against every API path in the matching bucket, so the compilations scaled with the size of the spec rather than the request. The pattern depends only on the API path template, which is fixed for the lifetime of the ApiPathImpl, so it is now compiled once in the constructor. --- .../oai/validator/model/ApiPathImpl.java | 11 ++++-- .../oai/validator/model/ApiPathImplTest.java | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/model/ApiPathImpl.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/model/ApiPathImpl.java index 372e589d..1acd36f4 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/model/ApiPathImpl.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/model/ApiPathImpl.java @@ -33,6 +33,9 @@ public class ApiPathImpl extends NormalisedPathImpl implements ApiPath { private final boolean strictPathMatching; + /** Compiled once per part - the path template is fixed for the lifetime of this instance. */ + private final Pattern[] partPatterns; + public ApiPathImpl(@Nonnull final String path, @Nullable final String apiPrefix) { this(path, apiPrefix, false); } @@ -45,6 +48,10 @@ public ApiPathImpl(@Nonnull final String path, final boolean strictPathMatching) { super(path, apiPrefix); this.strictPathMatching = strictPathMatching; + this.partPatterns = new Pattern[numberOfParts()]; + for (int i = 0; i < partPatterns.length; i++) { + partPatterns[i] = compile(quote(part(i)).replaceAll(PARAM_REGEX, "\\\\E(.*?)\\\\Q"), CASE_INSENSITIVE); + } } @Override @@ -66,9 +73,7 @@ public boolean matches(final NormalisedPath requestPath) { @Override public boolean partMatches(final int index, @Nonnull final String requestPathPart) { requireNonNull(requestPathPart, "A request path part is required"); - final String template = part(index); - final Pattern templatePattern = compile(quote(template).replaceAll(PARAM_REGEX, "\\\\E(.*?)\\\\Q"), CASE_INSENSITIVE); - return templatePattern.matcher(requestPathPart).matches(); + return partPatterns[index].matcher(requestPathPart).matches(); } @Override diff --git a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/model/ApiPathImplTest.java b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/model/ApiPathImplTest.java index fc968557..705dfa8f 100644 --- a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/model/ApiPathImplTest.java +++ b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/model/ApiPathImplTest.java @@ -10,6 +10,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; public class ApiPathImplTest { @@ -212,6 +213,44 @@ public void matches_doesNotMatches_whenBadParamMatch() { assertThat(matches("/p1/p2/{param1}-{param2}", "/p1/p2/floop"), is(false)); } + @Test + public void partMatches_matchesLiterally_whenPartContainsRegexMetacharacters() { + assertThat(partMatches("a.b", "a.b"), is(true)); + assertThat(partMatches("a.b", "axb"), is(false)); + assertThat(partMatches("a+b", "a+b"), is(true)); + assertThat(partMatches("a+b", "aab"), is(false)); + } + + @Test + public void partMatches_throws_whenIndexOutOfRange() { + final ApiPathImpl classUnderTest = new ApiPathImpl("/{id}", null); + + assertThrows(IndexOutOfBoundsException.class, () -> classUnderTest.partMatches(1, "foop")); + } + + @Test + public void partMatches_isRepeatable_onTheSameInstance() { + final ApiPathImpl classUnderTest = new ApiPathImpl("/{id}/literal/a.b/a+b", null); + + assertThat(classUnderTest.partMatches(0, "foop"), is(true)); + assertThat(classUnderTest.partMatches(1, "literal"), is(true)); + assertThat(classUnderTest.partMatches(1, "LITERAL"), is(true)); + assertThat(classUnderTest.partMatches(1, "blarp"), is(false)); + assertThat(classUnderTest.partMatches(2, "a.b"), is(true)); + assertThat(classUnderTest.partMatches(2, "axb"), is(false)); + assertThat(classUnderTest.partMatches(3, "a+b"), is(true)); + assertThat(classUnderTest.partMatches(3, "aab"), is(false)); + } + + @Test + public void matches_isRepeatable_onTheSameInstance() { + final ApiPathImpl classUnderTest = new ApiPathImpl("/p1/{param1}/p3", null); + + assertThat(classUnderTest.matches(new NormalisedPathImpl("/p1/foop/p3", null)), is(true)); + assertThat(classUnderTest.matches(new NormalisedPathImpl("/p1/foop/P3", null)), is(true)); + assertThat(classUnderTest.matches(new NormalisedPathImpl("/p1/foop/blarp", null)), is(false)); + } + private static void testParamValueExtraction(final String expression, final String path, final String... expected) { assertThat(new ApiPathImpl(expression, null).paramValues(0, path).values(), containsInAnyOrder(stream(expected).map(e -> is(ofNullable(e))).collect(toList())));