Skip to content

Commit d17c7e8

Browse files
authored
Merge pull request quarkusio#47386 from Postremus/issues/26496
Support overlapping paths on resource classes
2 parents d793ece + 6b67e05 commit d17c7e8

File tree

6 files changed

+213
-23
lines changed

6 files changed

+213
-23
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package io.quarkus.resteasy.reactive.server.test.resource.basic;
2+
3+
import static io.restassured.RestAssured.given;
4+
import static org.hamcrest.Matchers.equalTo;
5+
6+
import java.util.function.Supplier;
7+
8+
import jakarta.ws.rs.GET;
9+
import jakarta.ws.rs.Path;
10+
11+
import org.jboss.resteasy.reactive.RestPath;
12+
import org.jboss.shrinkwrap.api.ShrinkWrap;
13+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.RegisterExtension;
16+
17+
import io.quarkus.resteasy.reactive.server.test.simple.PortProviderUtil;
18+
import io.quarkus.test.QuarkusUnitTest;
19+
20+
class OverlappingResourceClassPathTest {
21+
@RegisterExtension
22+
static QuarkusUnitTest testExtension = new QuarkusUnitTest()
23+
.setArchiveProducer(new Supplier<>() {
24+
@Override
25+
public JavaArchive get() {
26+
JavaArchive war = ShrinkWrap.create(JavaArchive.class);
27+
war.addClasses(PortProviderUtil.class);
28+
war.addClasses(UsersResource.class);
29+
war.addClasses(UserResource.class);
30+
war.addClasses(GreetingResource.class);
31+
return war;
32+
}
33+
});
34+
35+
@Test
36+
void basicTest() {
37+
given()
38+
.get("/users/userId")
39+
.then()
40+
.statusCode(200)
41+
.body(equalTo("userId"));
42+
43+
given()
44+
.get("/users/userId/by-id")
45+
.then()
46+
.statusCode(200)
47+
.body(equalTo("getByIdInUserResource-userId"));
48+
}
49+
50+
@Path("/users")
51+
public static class UsersResource {
52+
53+
@GET
54+
@Path("{id}")
55+
public String getByIdInUsersResource(@RestPath String id) {
56+
return id;
57+
}
58+
}
59+
60+
@Path("/users/{id}")
61+
public static class UserResource {
62+
63+
@GET
64+
@Path("by-id")
65+
public String getByIdInUserResource(@RestPath String id) {
66+
return "getByIdInUserResource-" + id;
67+
}
68+
}
69+
70+
@Path("/i-do-not-match")
71+
public static class GreetingResource {
72+
73+
@GET
74+
@Path("greet")
75+
public String greet() {
76+
return "Hello";
77+
}
78+
}
79+
}

extensions/resteasy-reactive/rest/runtime/src/test/java/io/quarkus/resteasy/reactive/runtime/mapping/RequestMapperTestCase.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
public class RequestMapperTestCase {
1111

1212
@Test
13-
public void testPathMapper() {
13+
void testMap() {
1414

15-
RequestMapper<String> mapper = mapper("/id", "/id/{param}", "/bar/{p1}/{p2}", "/bar/{p1}");
15+
RequestMapper<String> mapper = mapper(false, "/id", "/id/{param}", "/bar/{p1}/{p2}", "/bar/{p1}");
1616
mapper.dump();
1717

1818
RequestMapper.RequestMatch<String> result = mapper.map("/bar/34/44");
@@ -31,13 +31,32 @@ public void testPathMapper() {
3131
result = mapper.map("/bar/34");
3232
Assertions.assertEquals("/bar/{p1}", result.value);
3333
Assertions.assertEquals("34", result.pathParamValues[0]);
34+
}
35+
36+
@Test
37+
public void testContinueMatching() {
38+
RequestMapper<String> mapper = mapper(true, "/greetings", "/greetings/{id}", "/greetings/unrelated");
39+
mapper.dump();
40+
41+
var result = mapper.map("/not-existing");
42+
Assertions.assertNull(result);
43+
44+
result = mapper.map("/greetings/greeting-id");
45+
Assertions.assertNotNull(result);
46+
Assertions.assertEquals("", result.remaining);
47+
48+
result = mapper.continueMatching("/greetings/greeting-id", result);
49+
Assertions.assertNotNull(result);
50+
Assertions.assertEquals("/greeting-id", result.remaining);
3451

52+
result = mapper.continueMatching("/greetings/greeting-id", result);
53+
Assertions.assertNull(result);
3554
}
3655

37-
RequestMapper<String> mapper(String... vals) {
56+
RequestMapper<String> mapper(boolean prefixTemplates, String... vals) {
3857
ArrayList<RequestMapper.RequestPath<String>> list = new ArrayList<>();
3958
for (String i : vals) {
40-
list.add(new RequestMapper.RequestPath<>(false, new URITemplate(i, false), i));
59+
list.add(new RequestMapper.RequestPath<>(prefixTemplates, new URITemplate(i, false), i));
4160
}
4261
return new RequestMapper<>(list);
4362
}

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.jboss.resteasy.reactive.server.SimpleResourceInfo;
5252
import org.jboss.resteasy.reactive.server.core.multipart.FormData;
5353
import org.jboss.resteasy.reactive.server.core.serialization.EntityWriter;
54+
import org.jboss.resteasy.reactive.server.handlers.RestInitialHandler;
5455
import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionContext;
5556
import org.jboss.resteasy.reactive.server.jaxrs.AsyncResponseImpl;
5657
import org.jboss.resteasy.reactive.server.jaxrs.ContainerRequestContextImpl;
@@ -62,6 +63,7 @@
6263
import org.jboss.resteasy.reactive.server.jaxrs.SseEventSinkImpl;
6364
import org.jboss.resteasy.reactive.server.jaxrs.SseImpl;
6465
import org.jboss.resteasy.reactive.server.jaxrs.UriInfoImpl;
66+
import org.jboss.resteasy.reactive.server.mapping.RequestMapper;
6567
import org.jboss.resteasy.reactive.server.mapping.RuntimeResource;
6668
import org.jboss.resteasy.reactive.server.mapping.URITemplate;
6769
import org.jboss.resteasy.reactive.server.multipart.FormValue;
@@ -156,6 +158,8 @@ public abstract class ResteasyReactiveRequestContext
156158
private FormData formData;
157159
private boolean producesChecked;
158160

161+
private RequestMapper.RequestMatch<RestInitialHandler.InitialMatch> initialMatch;
162+
159163
public ResteasyReactiveRequestContext(Deployment deployment,
160164
ThreadSetupAction requestContext, ServerRestHandler[] handlerChain, ServerRestHandler[] abortHandlerChain) {
161165
super(handlerChain, abortHandlerChain, requestContext);
@@ -203,6 +207,44 @@ public void restart(RuntimeResource target, boolean setLocatorTarget) {
203207
this.target = target;
204208
}
205209

210+
public void setupInitialMatchAndRestart(RequestMapper.RequestMatch<RestInitialHandler.InitialMatch> initialMatch) {
211+
this.initialMatch = initialMatch;
212+
213+
restart(initialMatch.value.handlers);
214+
setMaxPathParams(initialMatch.value.maxPathParams);
215+
setRemaining(initialMatch.remaining);
216+
for (int i = 0; i < initialMatch.pathParamValues.length; ++i) {
217+
String pathParamValue = initialMatch.pathParamValues[i];
218+
if (pathParamValue == null) {
219+
break;
220+
}
221+
setPathParamValue(i, initialMatch.pathParamValues[i]);
222+
}
223+
}
224+
225+
/**
226+
* Restarts handler chain processing if another initial match is found.
227+
*
228+
* @return true if a restart occurred
229+
*/
230+
public boolean restartWithNextInitialMatch() {
231+
initialMatch = new RequestMapper<>(deployment.getClassMappers()).continueMatching(getPathWithoutPrefix(), initialMatch);
232+
if (initialMatch == null) {
233+
return false;
234+
}
235+
restart(initialMatch.value.handlers);
236+
setMaxPathParams(initialMatch.value.maxPathParams);
237+
setRemaining(initialMatch.remaining);
238+
for (int i = 0; i < initialMatch.pathParamValues.length; ++i) {
239+
String pathParamValue = initialMatch.pathParamValues[i];
240+
if (pathParamValue == null) {
241+
break;
242+
}
243+
setPathParamValue(i, initialMatch.pathParamValues[i]);
244+
}
245+
return true;
246+
}
247+
206248
/**
207249
* Meant to be used when an error occurred early in processing chain
208250
*/

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
6666
mapper = mappers.get(null);
6767
}
6868
if (mapper == null) {
69+
if (requestContext.restartWithNextInitialMatch()) {
70+
return;
71+
}
6972
// The idea here is to check if any of the mappers of the class could map the request - if the HTTP Method were correct
7073
String remaining = getRemaining(requestContext);
7174
for (RequestMapper<RuntimeResource> existingMapper : mappers.values()) {
@@ -89,6 +92,9 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
8992
}
9093

9194
if (target == null) {
95+
if (requestContext.restartWithNextInitialMatch()) {
96+
return;
97+
}
9298
// The idea here is to check if any of the mappers of the class could map the request - if the HTTP Method were correct
9399
for (Map.Entry<String, RequestMapper<RuntimeResource>> entry : mappers.entrySet()) {
94100
if (entry.getKey() == null) {

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/RestInitialHandler.java

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,7 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
7373
return;
7474
}
7575
}
76-
requestContext.restart(target.value.handlers);
77-
requestContext.setMaxPathParams(target.value.maxPathParams);
78-
requestContext.setRemaining(target.remaining);
79-
for (int i = 0; i < target.pathParamValues.length; ++i) {
80-
String pathParamValue = target.pathParamValues[i];
81-
if (pathParamValue == null) {
82-
break;
83-
}
84-
requestContext.setPathParamValue(i, target.pathParamValues[i]);
85-
}
76+
requestContext.setupInitialMatchAndRestart(target);
8677
}
8778

8879
public static class InitialMatch {

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import java.util.Arrays;
55
import java.util.Collections;
66
import java.util.HashMap;
7-
import java.util.List;
87
import java.util.Map;
98
import java.util.function.BiConsumer;
109
import java.util.regex.Matcher;
@@ -42,30 +41,84 @@ public void accept(String stem, ArrayList<RequestPath<T>> list) {
4241
requestPaths = pathMatcherBuilder.build();
4342
}
4443

44+
/**
45+
* Match the path to the UriTemplates. Returns the best match, meaning the least remaining path after match.
46+
*
47+
* @param path path to search UriTemplate for
48+
* @return best RequestMatch, or null if the path has no match
49+
*/
4550
public RequestMatch<T> map(String path) {
46-
var result = mapFromPathMatcher(path, requestPaths.match(path));
51+
var result = mapFromPathMatcher(path, requestPaths.match(path), 0);
4752
if (result != null) {
4853
return result;
4954
}
5055

5156
// the following code is meant to handle cases like https://github.com/quarkusio/quarkus/issues/30667
52-
return mapFromPathMatcher(path, requestPaths.defaultMatch(path));
57+
return mapFromPathMatcher(path, requestPaths.defaultMatch(path), 0);
58+
}
59+
60+
/**
61+
* Continue matching for the next best path starting from the last match, meaning the least remaining path after match.
62+
*
63+
* @param path path to search UriTemplate for
64+
* @return another RequestMatch. Might return null if all matches are exhausted.
65+
*/
66+
public RequestMatch<T> continueMatching(String path, RequestMatch<T> lastMatch) {
67+
if (lastMatch == null) {
68+
return null;
69+
}
70+
71+
var initialMatches = requestPaths.match(path);
72+
var result = mapFromPathMatcher(path, initialMatches, 0);
73+
if (result != null) {
74+
int idx = nextMatchStartingIndex(initialMatches, lastMatch);
75+
return mapFromPathMatcher(path, initialMatches, idx);
76+
}
77+
78+
// the following code is meant to handle cases like https://github.com/quarkusio/quarkus/issues/30667
79+
initialMatches = requestPaths.defaultMatch(path);
80+
result = mapFromPathMatcher(path, initialMatches, 0);
81+
if (result != null) {
82+
int idx = nextMatchStartingIndex(initialMatches, lastMatch);
83+
return mapFromPathMatcher(path, initialMatches, idx);
84+
}
85+
return null;
86+
}
87+
88+
private int nextMatchStartingIndex(PathMatcher.PathMatch<ArrayList<RequestPath<T>>> initialMatches,
89+
RequestMatch<T> current) {
90+
if (initialMatches.getValue() == null || initialMatches.getValue().isEmpty()) {
91+
return -1;
92+
}
93+
for (int i = 0; i < initialMatches.getValue().size(); i++) {
94+
if (initialMatches.getValue().get(i).template == current.template) {
95+
i++;
96+
97+
if (i < initialMatches.getValue().size()) {
98+
return i;
99+
}
100+
return -1;
101+
}
102+
}
103+
104+
return -1;
53105
}
54106

55107
@SuppressWarnings({ "rawtypes", "unchecked" })
56-
private RequestMatch<T> mapFromPathMatcher(String path, PathMatcher.PathMatch<ArrayList<RequestPath<T>>> initialMatch) {
57-
var value = initialMatch.getValue();
58-
if (initialMatch.getValue() == null) {
108+
private RequestMatch<T> mapFromPathMatcher(String path, PathMatcher.PathMatch<ArrayList<RequestPath<T>>> initialMatches,
109+
int startIdx) {
110+
var value = initialMatches.getValue();
111+
if (value == null || startIdx < 0) {
59112
return null;
60113
}
61114
int pathLength = path.length();
62-
for (int index = 0; index < ((List<RequestPath<T>>) value).size(); index++) {
63-
RequestPath<T> potentialMatch = ((List<RequestPath<T>>) value).get(index);
115+
for (int index = startIdx; index < value.size(); index++) {
116+
RequestPath<T> potentialMatch = value.get(index);
64117
String[] params = (maxParams > 0) ? new String[maxParams] : EMPTY_STRING_ARRAY;
65118
int paramCount = 0;
66119
boolean matched = true;
67120
boolean prefixAllowed = potentialMatch.prefixTemplate;
68-
int matchPos = initialMatch.getMatched().length();
121+
int matchPos = initialMatches.getMatched().length();
69122
for (int i = 1; i < potentialMatch.template.components.length; ++i) {
70123
URITemplate.TemplateComponent segment = potentialMatch.template.components[i];
71124
if (segment.type == URITemplate.Type.CUSTOM_REGEX) {

0 commit comments

Comments
 (0)