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));
+        }
+    }
+}