Allow for matching multiple resources with equal paths as in 4119 (#4132)
* Allow for matching multiple resources with equal paths as reported in 4119.
Signed-off-by: Jan Supol <jan.supol@oracle.com>
diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java b/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java
index 6876f20..1ed213b 100644
--- a/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java
+++ b/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -102,10 +102,10 @@
}
// If the number of explicit characters and template variables
- // are equal then comapre the regexes
- // The order does not matter as long as templates with different
+ // are equal then
+ // the order does not matter as long as templates with different
// explicit characters are distinguishable
- return o2.pattern.getRegex().compareTo(o1.pattern.getRegex());
+ return 0;
}
};
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/MethodSelectingRouter.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/MethodSelectingRouter.java
index 8f41d22..e86f31b 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/MethodSelectingRouter.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/MethodSelectingRouter.java
@@ -156,6 +156,10 @@
}
}
+ Set<String> getHttpMethods() {
+ return consumesProducesAcceptors.keySet();
+ }
+
/**
* Represents a 1-1-1 relation between input and output media type and an methodAcceptorPair.
* <p>E.g. for a single resource method
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/PathMatchingRouter.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/PathMatchingRouter.java
index feb1fe2..c161ad8 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/PathMatchingRouter.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/PathMatchingRouter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -56,19 +56,31 @@
tracingLogger.log(ServerTraceEvent.MATCH_PATH_FIND, path);
Router.Continuation result = null;
+ MatchResult matchResultCandidate = null;
+ Route acceptedRouteCandidate = null;
+
final Iterator<Route> iterator = acceptedRoutes.iterator();
while (iterator.hasNext()) {
final Route acceptedRoute = iterator.next();
final PathPattern routePattern = acceptedRoute.routingPattern();
- final MatchResult m = routePattern.match(path);
- if (m != null) {
- // Push match result information and rest of path to match
- rc.pushMatchResult(m);
- result = Router.Continuation.of(context, acceptedRoute.next());
-
- //tracing
- tracingLogger.log(ServerTraceEvent.MATCH_PATH_SELECTED, routePattern.getRegex());
- break;
+ final MatchResult matchResult = routePattern.match(path);
+ if (matchResult != null) {
+ if (isLocator(acceptedRoute) && matchResultCandidate != null) {
+ // acceptedRoute matches the path but it is a locator
+ // sub-resource locator shall not be found by the Spec if sub-resource was found first
+ // need to use the sub-resource to return correct HTTP Status
+ // TODO configuration option not to fail and continue with acceptedRoute locator?
+ result = matchPathSelected(context, acceptedRouteCandidate, matchResultCandidate, tracingLogger);
+ break;
+ } else if (isLocator(acceptedRoute) || designatorMatch(acceptedRoute, context)) {
+ result = matchPathSelected(context, acceptedRoute, matchResult, tracingLogger);
+ break;
+ } else if (matchResultCandidate == null) {
+ // store the first matched candidate with unmatched method designator
+ // maybe there won't be a better sub-resource
+ matchResultCandidate = matchResult;
+ acceptedRouteCandidate = acceptedRoute;
+ }
} else {
tracingLogger.log(ServerTraceEvent.MATCH_PATH_NOT_MATCHED, routePattern.getRegex());
}
@@ -80,6 +92,11 @@
}
}
+ if (result == null && acceptedRouteCandidate != null) {
+ //method designator mismatched, but still go the route to get the proper status code
+ result = matchPathSelected(context, acceptedRouteCandidate, matchResultCandidate, tracingLogger);
+ }
+
if (result == null) {
// No match
return Router.Continuation.of(context);
@@ -87,4 +104,35 @@
return result;
}
+
+ private Router.Continuation matchPathSelected(final RequestProcessingContext context, final Route acceptedRoute,
+ final MatchResult matchResult, final TracingLogger tracingLogger) {
+ // Push match result information and rest of path to match
+ context.routingContext().pushMatchResult(matchResult);
+ final Router.Continuation result = Router.Continuation.of(context, acceptedRoute.next());
+
+ // tracing
+ tracingLogger.log(ServerTraceEvent.MATCH_PATH_SELECTED, acceptedRoute.routingPattern().getRegex());
+
+ return result;
+ }
+
+ /**
+ * Return {@code true} iff the sub-resource method designator does match the request http method designator
+ * @param route current route representing resource method / locator
+ * @param context Contains Request to check the http method
+ * @return false if method designator does not match
+ */
+ private static boolean designatorMatch(final Route route, final RequestProcessingContext context) {
+ final String httpMethod = context.request().getMethod();
+
+ if (route.getHttpMethods().contains(httpMethod)) {
+ return true;
+ }
+ return ("HEAD".equals(httpMethod) && route.getHttpMethods().contains("GET"));
+ }
+
+ private static boolean isLocator(final Route route) {
+ return route.getHttpMethods() == null;
+ }
}
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/PathMatchingRouterBuilder.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/PathMatchingRouterBuilder.java
index f18e1c7..8d4dc43 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/PathMatchingRouterBuilder.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/PathMatchingRouterBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -22,7 +22,6 @@
import org.glassfish.jersey.uri.PathPattern;
/**
- /**
* A request path pattern matching router hierarchy builder entry point.
*
* @author Paul Sandoz
@@ -66,13 +65,16 @@
@Override
public PathMatchingRouterBuilder to(final Router router) {
+ if (MethodSelectingRouter.class.isInstance(router)) {
+ acceptedRoutes.get(acceptedRoutes.size() - 1).setHttpMethods(((MethodSelectingRouter) router).getHttpMethods());
+ }
currentRouters.add(router);
return this;
}
/**
* Complete the currently built unfinished sub-route (if any) and start building a new one.
- *
+ * <p>
* The completed sub-route is added to the list of the routes accepted by the router that is being built.
*
* @param pattern routing pattern for the new sub-route.
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/Route.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/Route.java
index 2a89655..b489730 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/Route.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/routing/Route.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -17,6 +17,7 @@
package org.glassfish.jersey.server.internal.routing;
import java.util.List;
+import java.util.Set;
import org.glassfish.jersey.uri.PathPattern;
@@ -31,6 +32,8 @@
private final PathPattern routingPattern;
private final List<Router> routers;
+ private Set<String> httpMethods;
+
/**
* Create a new request route.
*
@@ -68,4 +71,12 @@
public List<Router> next() {
return routers;
}
+
+ Set<String> getHttpMethods() {
+ return httpMethods;
+ }
+
+ void setHttpMethods(Set<String> httpMethods) {
+ this.httpMethods = httpMethods;
+ }
}
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/model/RuntimeResource.java b/core-server/src/main/java/org/glassfish/jersey/server/model/RuntimeResource.java
index 8b46ace..1001c25 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/model/RuntimeResource.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/model/RuntimeResource.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -128,11 +128,20 @@
public static final Comparator<RuntimeResource> COMPARATOR = new Comparator<RuntimeResource>() {
@Override
public int compare(RuntimeResource o1, RuntimeResource o2) {
- return PathPattern.COMPARATOR.compare(o1.getPathPattern(), o2.getPathPattern());
+ final int cmp = PathPattern.COMPARATOR.compare(o1.getPathPattern(), o2.getPathPattern());
+ if (cmp == 0) {
+ // quaternary key sorting those derived from
+ // sub-resource methods ahead of those derived from sub-resource locators
+ final int locatorCmp = o1.resourceLocators.size() - o2.resourceLocators.size();
+
+ // compare the regexes if still equal
+ return (locatorCmp == 0) ? o2.regex.compareTo(o1.regex) : locatorCmp;
+ } else {
+ return cmp;
+ }
}
};
-
private final String regex;
private final List<ResourceMethod> resourceMethods;
private final List<ResourceMethod> resourceLocators;
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/routing/RegularExpressionsTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/routing/RegularExpressionsTest.java
new file mode 100644
index 0000000..e5f31dc
--- /dev/null
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/routing/RegularExpressionsTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.server.routing;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class RegularExpressionsTest extends JerseyTest {
+ private static final String GET_VALUE = "get value";
+ private static final String POST_VALUE = "post value";
+ private static final String PUT_VALUE = "put value";
+
+
+ @Path("one")
+ public static class ResourceOne {
+ @POST
+ public String post(String entity) {
+ return entity;
+ }
+
+ @GET
+ @Path("x")
+ public Response get() {
+ return Response.ok(GET_VALUE).build();
+
+ }
+
+ @POST
+ @Path("{name:[a-zA-Z][a-zA-Z_0-9]*}")
+ public Response post() {
+ return Response.ok(POST_VALUE).build();
+
+ }
+
+ @Path("{x:[a-z]}")
+ public SubGet doAnything4() {
+ return new SubGet();
+ }
+ }
+
+ @Path("two")
+ public static class ResourceTwo {
+ @GET
+ @Path("{Prefix}{p:/?}{id: ((\\d+)?)}/abc{p2:/?}{number: (([A-Za-z0-9]*)?)}")
+ public Response get() {
+ return Response.ok(GET_VALUE).build();
+
+ }
+
+ @POST
+ @Path("{Prefix}{p:/?}{id: ((\\d+)?)}/abc/{yeah}")
+ public Response post() {
+ return Response.ok(POST_VALUE).build();
+
+ }
+ }
+
+ public static class SubGet {
+ @PUT
+ public String get() {
+ return PUT_VALUE;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(ResourceOne.class, ResourceTwo.class);
+ }
+
+ @Test
+ public void testPostOne() {
+ String entity = target("one").path("x").request()
+ .buildPost(Entity.entity("AA", MediaType.TEXT_PLAIN_TYPE)).invoke().readEntity(String.class);
+ assertThat(entity, is(POST_VALUE));
+ }
+
+ @Test
+ public void testGetOne() {
+ String entity = target("one").path("x").request().buildGet().invoke().readEntity(String.class);
+ assertThat(entity, is(GET_VALUE));
+ }
+
+ @Test
+ public void testPostTwo() {
+ String entity = target("two").path("P/abc/MyNumber").request()
+ .buildPost(Entity.entity("AA", MediaType.TEXT_PLAIN_TYPE)).invoke().readEntity(String.class);
+ assertThat(entity, is(POST_VALUE));
+ }
+
+ @Test
+ public void testGetTwo() {
+ String entity = target("two").path("P/abc/MyNumber").request().buildGet().invoke().readEntity(String.class);
+ assertThat(entity, is(GET_VALUE));
+ }
+
+ @Test
+ /**
+ * By the Spec, sub-resource locator should not be found in this case
+ */
+ public void testPutOne() {
+ try (Response response = target("one").path("x").request()
+ .buildPut(Entity.entity("AA", MediaType.TEXT_PLAIN_TYPE)).invoke()) {
+ assertThat(response.getStatus(), is(405));
+ }
+ }
+}