diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java index 3ef5ad8f2d11c..9206d4f93e0b8 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java @@ -131,7 +131,7 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) { //it is possible that multiple resource classes use the same path //we use this map to merge them - Map<URITemplate, Map<String, TreeMap<URITemplate, List<RequestMapper.RequestPath<RuntimeResource>>>>> mappers = new TreeMap<>(); + Map<MappersKey, Map<String, TreeMap<URITemplate, List<RequestMapper.RequestPath<RuntimeResource>>>>> mappers = new TreeMap<>(); for (int i = 0; i < resourceClasses.size(); i++) { ResourceClass clazz = resourceClasses.get(i); @@ -139,9 +139,12 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) { continue; } URITemplate classTemplate = new URITemplate(clazz.getPath(), true); - var perClassMappers = mappers.get(classTemplate); + + MappersKey key = new MappersKey(classTemplate); + + var perClassMappers = mappers.get(key); if (perClassMappers == null) { - mappers.put(classTemplate, perClassMappers = new HashMap<>()); + mappers.put(key, perClassMappers = new HashMap<>()); } for (int j = 0; j < clazz.getMethods().size(); j++) { ResourceMethod method = clazz.getMethods().get(j); @@ -153,6 +156,7 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) { } } + classMappers = new ArrayList<>(mappers.size()); mappers.forEach(this::forEachMapperEntry); @@ -208,14 +212,14 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) { runtimeConfigurableServerRestHandlers, exceptionMapper, info.isResumeOn404(), info.getResteasyReactiveConfig()); } - private void forEachMapperEntry(URITemplate path, + private void forEachMapperEntry(MappersKey key, Map<String, TreeMap<URITemplate, List<RequestMapper.RequestPath<RuntimeResource>>>> classTemplates) { - int classTemplateNameCount = path.countPathParamNames(); + int classTemplateNameCount = key.path.countPathParamNames(); RuntimeMappingDeployment runtimeMappingDeployment = new RuntimeMappingDeployment(classTemplates); ClassRoutingHandler classRoutingHandler = new ClassRoutingHandler(runtimeMappingDeployment.buildClassMapper(), classTemplateNameCount, info.isResumeOn404()); - classMappers.add(new RequestMapper.RequestPath<>(true, path, + classMappers.add(new RequestMapper.RequestPath<>(true, key.path, new RestInitialHandler.InitialMatch(new ServerRestHandler[] { classRoutingHandler }, runtimeMappingDeployment.getMaxMethodTemplateNameCount() + classTemplateNameCount))); } @@ -267,4 +271,69 @@ private String sanitizePathPrefix(String prefix) { return prefix; } + private static class MappersKey implements Comparable<MappersKey> { + private final String key; + private final URITemplate path; + + public MappersKey(URITemplate path) { + this.path = path; + + if (path.components.length == 0) { + this.key = ""; + } else { + // create a key without any names. Names of e.g. default regex components can differ, but the component still has the same meaning. + StringBuilder keyBuilder = new StringBuilder(); + for (URITemplate.TemplateComponent component : path.components) { + int standardLength = component.type.name().length() + 1 + + (component.literalText != null ? component.literalText.length() : 0) + 1 + 1; + int additionalLength = 0; + if (component.pattern != null) { + additionalLength = component.pattern.pattern().length(); + } + StringBuilder kb = new StringBuilder(standardLength + additionalLength); + kb.append(component.type); + kb.append(";"); + kb.append(component.literalText); + kb.append(";"); + if (component.pattern != null) { + // (?<id1>[a-zA-Z]+) -> [a-zA-Z]+ + String pattern = component.pattern.pattern(); + kb.append(component.pattern.pattern(), pattern.indexOf('>') + 1, pattern.length() - 1); + } + kb.append("|"); + keyBuilder.append(kb); + } + + this.key = keyBuilder.toString(); + } + + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null) { + return false; + } + + return key.equals(((MappersKey) o).key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public int compareTo(MappersKey o) { + if (key.equals(o.key)) { + return 0; + } + + return path.compareTo(o.path); + } + } + } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/ResourceClassMergeTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/ResourceClassMergeTest.java new file mode 100644 index 0000000000000..b5ae2b66821cb --- /dev/null +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/ResourceClassMergeTest.java @@ -0,0 +1,103 @@ +package org.jboss.resteasy.reactive.server.vertx.test.matching; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class ResourceClassMergeTest { + @RegisterExtension + static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(MatchDefaultRegexDifferentNameResourceA.class, + MatchDefaultRegexDifferentNameResourceB.class, MatchCustomRegexDifferentNameResourceA.class, + MatchCustomRegexDifferentNameResourceB.class); + } + }); + + @Test + public void testCallMatchDefaultRegexDifferentNameResource() { + given() + .when().get("routing-broken/abc/some/other/path") + .then() + .statusCode(200) + .body(is("abc")); + + given() + .when().get("routing-broken/efg/some/path") + .then() + .statusCode(200) + .body(is("efg")); + } + + @Test + public void testCallMatchCustomRegexDifferentNameResource() { + given() + .when().get("routing-broken-custom-regex/abc/some/other/path") + .then() + .statusCode(200) + .body(is("abc")); + + given() + .when().get("routing-broken-custom-regex/efg/some/path") + .then() + .statusCode(200) + .body(is("efg")); + } + + @Path("/routing-broken/{id1}") + public static class MatchDefaultRegexDifferentNameResourceA { + @GET + @Path("/some/other/path") + @Produces(MediaType.TEXT_PLAIN) + public Response doSomething(@PathParam("id1") String id) { + return Response.ok(id).build(); + } + } + + @Path("/routing-broken/{id}") + public static class MatchDefaultRegexDifferentNameResourceB { + @GET + @Path("/some/path") + @Produces(MediaType.TEXT_PLAIN) + public Response doSomething(@PathParam("id") String id) { + return Response.ok(id).build(); + } + } + + @Path("/routing-broken-custom-regex/{id1: [a-zA-Z]+}") + public static class MatchCustomRegexDifferentNameResourceA { + @GET + @Path("/some/other/path") + @Produces(MediaType.TEXT_PLAIN) + public Response doSomething(@PathParam("id1") String id) { + return Response.ok(id).build(); + } + } + + @Path("/routing-broken-custom-regex/{id: [a-zA-Z]+}") + public static class MatchCustomRegexDifferentNameResourceB { + @GET + @Path("/some/path") + @Produces(MediaType.TEXT_PLAIN) + public Response doSomething(@PathParam("id") String id) { + return Response.ok(id).build(); + } + } +}