Skip to content

Commit 943c446

Browse files
committed
Support overlapping paths on resource classes
1 parent 27e67c0 commit 943c446

File tree

6 files changed

+200
-23
lines changed

6 files changed

+200
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
import jakarta.ws.rs.core.Response;
11+
12+
import org.jboss.resteasy.reactive.RestPath;
13+
import org.jboss.resteasy.reactive.RestResponse;
14+
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
15+
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
16+
import org.jboss.resteasy.reactive.server.handlers.RestInitialHandler;
17+
import org.jboss.resteasy.reactive.server.mapping.RequestMapper;
18+
import org.jboss.resteasy.reactive.server.spi.ServerRequestContext;
19+
import org.jboss.shrinkwrap.api.ShrinkWrap;
20+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.RegisterExtension;
23+
24+
import io.quarkus.arc.ClientProxy;
25+
import io.quarkus.resteasy.reactive.server.test.simple.PortProviderUtil;
26+
import io.quarkus.test.QuarkusUnitTest;
27+
28+
class OverlappingResourceClassPathTest {
29+
@RegisterExtension
30+
static QuarkusUnitTest testExtension = new QuarkusUnitTest()
31+
.setArchiveProducer(new Supplier<>() {
32+
@Override
33+
public JavaArchive get() {
34+
JavaArchive war = ShrinkWrap.create(JavaArchive.class);
35+
war.addClasses(PortProviderUtil.class);
36+
war.addClasses(UsersResource.class);
37+
war.addClasses(UserResource.class);
38+
war.addClasses(GreetingResource.class);
39+
return war;
40+
}
41+
});
42+
43+
@Test
44+
void basicTest() {
45+
given()
46+
.get("/users/userId")
47+
.then()
48+
.statusCode(200)
49+
.body(equalTo("userId"));
50+
51+
given()
52+
.get("/users/userId/by-id")
53+
.then()
54+
.statusCode(200)
55+
.body(equalTo("getByIdInUserResource-userId"));
56+
57+
// test that only the User, and UsersResource have matched, and that the initial matches are sorted by remaining length
58+
given()
59+
.get("/users/userId/resource-does-not-exist")
60+
.then()
61+
.statusCode(404)
62+
.body(equalTo("/resource-does-not-exist|/userId/resource-does-not-exist|"));
63+
}
64+
65+
@Path("/users")
66+
public static class UsersResource {
67+
68+
@GET
69+
@Path("{id}")
70+
public String getByIdInUsersResource(@RestPath String id) {
71+
return id;
72+
}
73+
}
74+
75+
@Path("/users/{id}")
76+
public static class UserResource {
77+
78+
@GET
79+
@Path("by-id")
80+
public String getByIdInUserResource(@RestPath String id) {
81+
return "getByIdInUserResource-" + id;
82+
}
83+
}
84+
85+
@Path("/i-do-not-match")
86+
public static class GreetingResource {
87+
88+
@GET
89+
@Path("greet")
90+
public String greet() {
91+
return "Hello";
92+
}
93+
}
94+
95+
@ServerExceptionMapper
96+
public RestResponse<String> handle(RuntimeException ignored, ServerRequestContext requestContext) {
97+
ResteasyReactiveRequestContext ctxt = (ResteasyReactiveRequestContext) ClientProxy.unwrap(requestContext);
98+
String remainings = "";
99+
for (RequestMapper.RequestMatch<RestInitialHandler.InitialMatch> initialMatchRequestMatch : ctxt.getInitialMatches()) {
100+
remainings += initialMatchRequestMatch.remaining + "|";
101+
}
102+
return RestResponse.status(Response.Status.NOT_FOUND, remainings);
103+
}
104+
}

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

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.quarkus.resteasy.reactive.runtime.mapping;
22

33
import java.util.ArrayList;
4+
import java.util.List;
45

56
import org.jboss.resteasy.reactive.server.mapping.RequestMapper;
67
import org.jboss.resteasy.reactive.server.mapping.URITemplate;
@@ -10,9 +11,9 @@
1011
public class RequestMapperTestCase {
1112

1213
@Test
13-
public void testPathMapper() {
14+
public void testMap() {
1415

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

1819
RequestMapper.RequestMatch<String> result = mapper.map("/bar/34/44");
@@ -31,13 +32,23 @@ public void testPathMapper() {
3132
result = mapper.map("/bar/34");
3233
Assertions.assertEquals("/bar/{p1}", result.value);
3334
Assertions.assertEquals("34", result.pathParamValues[0]);
35+
}
36+
37+
@Test
38+
public void testAllMatches() {
39+
RequestMapper<String> mapper = mapper(true, "/greetings", "/greetings/{id}", "/greetings/unrelated");
40+
mapper.dump();
3441

42+
List<RequestMapper.RequestMatch<String>> result = mapper.allMatches("/greetings/greeting-id");
43+
Assertions.assertEquals(2, result.size());
44+
Assertions.assertEquals("", result.get(0).remaining);
45+
Assertions.assertEquals("/greeting-id", result.get(1).remaining);
3546
}
3647

37-
RequestMapper<String> mapper(String... vals) {
48+
RequestMapper<String> mapper(boolean prefixTemplates, String... vals) {
3849
ArrayList<RequestMapper.RequestPath<String>> list = new ArrayList<>();
3950
for (String i : vals) {
40-
list.add(new RequestMapper.RequestPath<>(false, new URITemplate(i, false), i));
51+
list.add(new RequestMapper.RequestPath<>(prefixTemplates, new URITemplate(i, false), i));
4152
}
4253
return new RequestMapper<>(list);
4354
}

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

+38
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,9 @@ public abstract class ResteasyReactiveRequestContext
156158
private FormData formData;
157159
private boolean producesChecked;
158160

161+
private List<RequestMapper.RequestMatch<RestInitialHandler.InitialMatch>> initialMatches;
162+
private int initialMatchIdx = 0;
163+
159164
public ResteasyReactiveRequestContext(Deployment deployment,
160165
ThreadSetupAction requestContext, ServerRestHandler[] handlerChain, ServerRestHandler[] abortHandlerChain) {
161166
super(handlerChain, abortHandlerChain, requestContext);
@@ -203,6 +208,31 @@ public void restart(RuntimeResource target, boolean setLocatorTarget) {
203208
this.target = target;
204209
}
205210

211+
/**
212+
* Restarts handler chain processing if another initial match exists. Initial matches are determined in RestInitialHandler.
213+
*
214+
* @return true if a restart occurred
215+
*/
216+
public boolean restartWithNextInitialMatch() {
217+
if (initialMatches == null || initialMatchIdx >= initialMatches.size()) {
218+
return false;
219+
}
220+
RequestMapper.RequestMatch<RestInitialHandler.InitialMatch> initialMatch = initialMatches
221+
.get(initialMatchIdx);
222+
restart(initialMatch.value.handlers);
223+
setMaxPathParams(initialMatch.value.maxPathParams);
224+
setRemaining(initialMatch.remaining);
225+
for (int i = 0; i < initialMatch.pathParamValues.length; ++i) {
226+
String pathParamValue = initialMatch.pathParamValues[i];
227+
if (pathParamValue == null) {
228+
break;
229+
}
230+
setPathParamValue(i, initialMatch.pathParamValues[i]);
231+
}
232+
initialMatchIdx++;
233+
return true;
234+
}
235+
206236
/**
207237
* Meant to be used when an error occurred early in processing chain
208238
*/
@@ -1254,6 +1284,14 @@ private String getResourceLocatorPathParam(String name, PreviousResource previou
12541284

12551285
public abstract boolean resumeExternalProcessing();
12561286

1287+
public List<RequestMapper.RequestMatch<RestInitialHandler.InitialMatch>> getInitialMatches() {
1288+
return initialMatches;
1289+
}
1290+
1291+
public void setInitialMatches(List<RequestMapper.RequestMatch<RestInitialHandler.InitialMatch>> initialMatches) {
1292+
this.initialMatches = initialMatches;
1293+
}
1294+
12571295
static class PreviousResource {
12581296

12591297
private static final String PROPERTY_KEY = AbstractResteasyReactiveContext.CUSTOM_RR_PROPERTIES_PREFIX

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

+6
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

+4-12
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ public void beginProcessing(Object externalHttpContext, Throwable throwable) {
5858

5959
@Override
6060
public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
61-
RequestMapper.RequestMatch<InitialMatch> target = mappers.map(requestContext.getPathWithoutPrefix());
62-
if (target == null) {
61+
List<RequestMapper.RequestMatch<InitialMatch>> targets = mappers.allMatches(requestContext.getPathWithoutPrefix());
62+
if (targets.isEmpty()) {
6363
ProvidersImpl providers = requestContext.getProviders();
6464
ExceptionMapper<NotFoundException> exceptionMapper = providers.getExceptionMapper(NotFoundException.class);
6565

@@ -73,16 +73,8 @@ 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.setInitialMatches(targets);
77+
requestContext.restartWithNextInitialMatch();
8678
}
8779

8880
public static class InitialMatch {

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

+33-7
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import java.util.ArrayList;
44
import java.util.Arrays;
55
import java.util.Collections;
6+
import java.util.Comparator;
67
import java.util.HashMap;
78
import java.util.List;
89
import java.util.Map;
910
import java.util.function.BiConsumer;
11+
import java.util.function.ToIntFunction;
1012
import java.util.regex.Matcher;
1113

1214
public class RequestMapper<T> {
@@ -43,8 +45,22 @@ public void accept(String stem, ArrayList<RequestPath<T>> list) {
4345
}
4446

4547
public RequestMatch<T> map(String path) {
48+
var result = allMatches(path);
49+
if (result.isEmpty()) {
50+
return null;
51+
}
52+
return result.get(0);
53+
}
54+
55+
/**
56+
* Retrieve all UriTemplate matches for a given path, ordered by how much of the path is remaining
57+
*
58+
* @param path path to search UriTemplate for
59+
* @return list, never null, may be empty
60+
*/
61+
public List<RequestMatch<T>> allMatches(String path) {
4662
var result = mapFromPathMatcher(path, requestPaths.match(path));
47-
if (result != null) {
63+
if (!result.isEmpty()) {
4864
return result;
4965
}
5066

@@ -53,14 +69,16 @@ public RequestMatch<T> map(String path) {
5369
}
5470

5571
@SuppressWarnings({ "rawtypes", "unchecked" })
56-
private RequestMatch<T> mapFromPathMatcher(String path, PathMatcher.PathMatch<ArrayList<RequestPath<T>>> initialMatch) {
72+
private List<RequestMatch<T>> mapFromPathMatcher(String path,
73+
PathMatcher.PathMatch<ArrayList<RequestPath<T>>> initialMatch) {
5774
var value = initialMatch.getValue();
5875
if (initialMatch.getValue() == null) {
59-
return null;
76+
return Collections.emptyList();
6077
}
6178
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);
79+
List<RequestMatch<T>> matches = new ArrayList<>();
80+
for (int index = 0; index < value.size(); index++) {
81+
RequestPath<T> potentialMatch = value.get(index);
6482
String[] params = (maxParams > 0) ? new String[maxParams] : EMPTY_STRING_ARRAY;
6583
int paramCount = 0;
6684
boolean matched = true;
@@ -138,10 +156,18 @@ private RequestMatch<T> mapFromPathMatcher(String path, PathMatcher.PathMatch<Ar
138156
remaining = path.substring(matchPos);
139157
}
140158
}
141-
return new RequestMatch(potentialMatch.template, potentialMatch.value, params, remaining);
159+
matches.add(new RequestMatch(potentialMatch.template, potentialMatch.value, params, remaining));
142160
}
143161
}
144-
return null;
162+
163+
matches.sort(Comparator.comparingInt(new ToIntFunction<RequestMatch<T>>() {
164+
@Override
165+
public int applyAsInt(RequestMatch<T> match) {
166+
return match.remaining.length();
167+
}
168+
}));
169+
170+
return matches;
145171
}
146172

147173
public static class RequestPath<T> implements Dumpable, Comparable<RequestPath<T>> {

0 commit comments

Comments
 (0)