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 8f41d22bec2..cbaf5ecd23e 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 @@ private int compare(List mediaTypeList1, List mediaTypeLis } } + /* package */ Set getHttpMethods() { + return consumesProducesAcceptors.keySet(); + } + /** * Represents a 1-1-1 relation between input and output media type and an methodAcceptorPair. *

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 feb1fe25448..2bd4cfadc17 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 @@ -16,14 +16,18 @@ package org.glassfish.jersey.server.internal.routing; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.regex.MatchResult; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.glassfish.jersey.message.internal.TracingLogger; import org.glassfish.jersey.server.internal.ServerTraceEvent; import org.glassfish.jersey.server.internal.process.RequestProcessingContext; import org.glassfish.jersey.uri.PathPattern; +import org.glassfish.jersey.uri.UriTemplate; /** * Matches the un-matched right-hand request path to the configured collection of path pattern matching routes. @@ -34,6 +38,7 @@ final class PathMatchingRouter implements Router { private final List acceptedRoutes; + private static final RouteComparator ROUTE_COMPARATOR = new RouteComparator(); /** * Constructs route methodAcceptorPair that uses {@link PathPattern} instances for @@ -56,15 +61,26 @@ public Router.Continuation apply(final RequestProcessingContext context) { tracingLogger.log(ServerTraceEvent.MATCH_PATH_FIND, path); Router.Continuation result = null; - final Iterator iterator = acceptedRoutes.iterator(); + //need sort if regexes in the @Path, keep original sort if no regexes +// final boolean needSort = acceptedRoutes +// .stream() +// .map(a -> a.routingPattern().getTemplate().getNumberOfExplicitRegexes()) +// .reduce(0, Integer::sum) > 0; + final boolean needSort = true; + + final Stream comparableRoutes = acceptedRoutes + .stream() + .map(a -> new ComparableRoute(a, context, path, needSort)); + final Stream sortedRoutes = needSort ? comparableRoutes.sorted(ROUTE_COMPARATOR) : comparableRoutes; + final Iterator iterator = sortedRoutes.iterator(); + while (iterator.hasNext()) { - final Route acceptedRoute = iterator.next(); - final PathPattern routePattern = acceptedRoute.routingPattern(); - final MatchResult m = routePattern.match(path); - if (m != null) { + final ComparableRoute acceptedRoute = iterator.next(); + final PathPattern routePattern = acceptedRoute.route.routingPattern(); + if (acceptedRoute.matchResult != null) { // Push match result information and rest of path to match - rc.pushMatchResult(m); - result = Router.Continuation.of(context, acceptedRoute.next()); + rc.pushMatchResult(acceptedRoute.matchResult); + result = Router.Continuation.of(context, acceptedRoute.route.next()); //tracing tracingLogger.log(ServerTraceEvent.MATCH_PATH_SELECTED, routePattern.getRegex()); @@ -76,7 +92,7 @@ public Router.Continuation apply(final RequestProcessingContext context) { if (tracingLogger.isLogEnabled(ServerTraceEvent.MATCH_PATH_SKIPPED)) { while (iterator.hasNext()) { - tracingLogger.log(ServerTraceEvent.MATCH_PATH_SKIPPED, iterator.next().routingPattern().getRegex()); + tracingLogger.log(ServerTraceEvent.MATCH_PATH_SKIPPED, iterator.next().route.routingPattern().getRegex()); } } @@ -87,4 +103,81 @@ public Router.Continuation apply(final RequestProcessingContext context) { return result; } + + private static class ComparableRoute { + private final Route route; + private final MatchResult matchResult; + private final boolean hasMethodSelectingRouter; + private final boolean hasRequestedMethodDesignator; + + /** + * Route decorator for possible sort + * @param route The original {@link Route} + * @param context The context containing request method + * @param path The path to be matched + * @param needDesignator Designator is needed only when sort is needed, i.e. when regexes in @Path + */ + private ComparableRoute(Route route, RequestProcessingContext context, String path, boolean needDesignator) { + this.route = route; + this.matchResult = route.routingPattern().match(path); + final List routers = !needDesignator || matchResult == null ? null : route + .next() + .stream() + .filter(MethodSelectingRouter.class::isInstance) + .map(MethodSelectingRouter.class::cast) + .collect(Collectors.toList()); + this.hasMethodSelectingRouter = !(routers == null || routers.isEmpty()); + this.hasRequestedMethodDesignator = !hasMethodSelectingRouter + ? false : routers.stream().anyMatch(a -> a.getHttpMethods().contains(context.request().getMethod())); + } + } + + /** + * There {@link Route routes} can be multiple routes each accessible by a request, in case + * different regular expressions gets matched by the request Uri. In that case, the routes are + * sorted so that the ones that do not have a proper method designator are at the bottom of the list. + * This step corresponds to Step 3 in Request Matching Algorithm. Also, the sort respects sorting by + * the regular expressions (UriTemplates) as mandated by the Algorithm. + */ + private static class RouteComparator implements Comparator { + + @Override + public int compare(ComparableRoute r1, ComparableRoute r2) { + if (r2.matchResult == null) { + return -1; + } + if (r1.matchResult == null) { + return 1; + } + if (!r2.hasMethodSelectingRouter && r1.hasMethodSelectingRouter) { + return -1; + } + if (r2.hasMethodSelectingRouter && !r1.hasMethodSelectingRouter) { + return 1; + } + if (!r2.hasRequestedMethodDesignator && r1.hasRequestedMethodDesignator) { + return -1; + } + if (r2.hasRequestedMethodDesignator && !r1.hasRequestedMethodDesignator) { + return 1; + } + return compareTemplates(r1.route.routingPattern().getTemplate(), r2.route.routingPattern().getTemplate()); + } + + private int compareTemplates(UriTemplate t1, UriTemplate t2) { + if (t1.getNumberOfExplicitCharacters() > t2.getNumberOfExplicitCharacters()) { + return -1; + } + if (t1.getNumberOfExplicitCharacters() < t2.getNumberOfExplicitCharacters()) { + return 1; + } + if (t1.getNumberOfExplicitRegexes() > t2.getNumberOfExplicitRegexes()) { + return -1; + } + if (t1.getNumberOfExplicitRegexes() < t2.getNumberOfExplicitRegexes()) { + return 1; + } + return t1.getNumberOfRegexGroups() - t2.getNumberOfRegexGroups() > 0 ? -1 : 1; + } + } } 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 00000000000..a0150472a21 --- /dev/null +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/routing/RegularExpressionsTest.java @@ -0,0 +1,121 @@ +/* + * 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 { + public static String GET_VALUE = "get value"; + public static String POST_VALUE = "post 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 "TEST"; + } + } + + @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)); + } +}