Repository: camel Updated Branches: refs/heads/camel-2.15.x a93c74c41 -> 517db2078
[CAMEL-8685] Netty HTTP resolves OPTIONS prefix matches earlier (cherry picked from commit 5b27ecc) Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/517db207 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/517db207 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/517db207 Branch: refs/heads/camel-2.15.x Commit: 517db207815e36c32cdd510bed84b85df3b8b54a Parents: a93c74c Author: Henryk Konsek <hekon...@gmail.com> Authored: Wed Apr 22 13:44:00 2015 +0200 Committer: Henryk Konsek <hekon...@gmail.com> Committed: Wed Apr 22 13:50:24 2015 +0200 ---------------------------------------------------------------------- .../netty/http/RestContextPathMatcher.java | 4 ++ .../HttpServerMultiplexChannelHandler.java | 44 ++++++++++++-- .../NettyHttpRestContextPathMatcherTest.java | 63 ++++++++++++++++++++ .../netty4/http/RestContextPathMatcher.java | 4 ++ .../HttpServerMultiplexChannelHandler.java | 45 ++++++++++++-- .../NettyHttpRestContextPathMatcherTest.java | 63 ++++++++++++++++++++ 6 files changed, 211 insertions(+), 12 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/517db207/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/RestContextPathMatcher.java ---------------------------------------------------------------------- diff --git a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/RestContextPathMatcher.java b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/RestContextPathMatcher.java index efc9258..30ab534 100644 --- a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/RestContextPathMatcher.java +++ b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/RestContextPathMatcher.java @@ -74,6 +74,10 @@ public class RestContextPathMatcher extends DefaultContextPathMatcher { consumerPath = consumerPath.substring(0, consumerPath.length() - 1); } + if (matchOnUriPrefix && (requestPath.startsWith(consumerPath) || consumerPath.isEmpty())) { + return true; + } + // split using single char / is optimized in the jdk String[] requestPaths = requestPath.split("/"); String[] consumerPaths = consumerPath.split("/"); http://git-wip-us.apache.org/repos/asf/camel/blob/517db207/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java ---------------------------------------------------------------------- diff --git a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java index 07de856..2c1a753 100644 --- a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java +++ b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java @@ -19,6 +19,7 @@ package org.apache.camel.component.netty.http.handlers; import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -176,18 +177,26 @@ public class HttpServerMultiplexChannelHandler extends SimpleChannelUpstreamHand } // then see if we got a direct match - Iterator<Map.Entry<ContextPathMatcher, HttpServerChannelHandler>> it = candidates.iterator(); - while (it.hasNext()) { - Map.Entry<ContextPathMatcher, HttpServerChannelHandler> entry = it.next(); + List<HttpServerChannelHandler> directMatches = new LinkedList<HttpServerChannelHandler>(); + for (Map.Entry<ContextPathMatcher, HttpServerChannelHandler> entry : candidates) { if (entry.getKey().matchesRest(path, false)) { - answer = entry.getValue(); - break; + directMatches.add(entry.getValue()); + } + } + if (directMatches.size() == 1) { // Single match found, just return it without any further analysis. + answer = directMatches.get(0); + } else if (directMatches.size() > 1) { // possible if the prefix match occurred + List<HttpServerChannelHandler> directMatchesWithOptions = handlersWithExplicitOptionsMethod(directMatches); + if (!directMatchesWithOptions.isEmpty()) { // prefer options matches + answer = handlerWithTheLongestMatchingPrefix(directMatchesWithOptions); + } else { + answer = handlerWithTheLongestMatchingPrefix(directMatches); } } // then match by non wildcard path if (answer == null) { - it = candidates.iterator(); + Iterator<Map.Entry<ContextPathMatcher, HttpServerChannelHandler>> it = candidates.iterator(); while (it.hasNext()) { Map.Entry<ContextPathMatcher, HttpServerChannelHandler> entry = it.next(); if (!entry.getKey().matchesRest(path, true)) { @@ -234,4 +243,27 @@ public class HttpServerMultiplexChannelHandler extends SimpleChannelUpstreamHand return UnsafeUriCharactersEncoder.encodeHttpURI(path); } + private static List<HttpServerChannelHandler> handlersWithExplicitOptionsMethod(Iterable<HttpServerChannelHandler> handlers) { + List<HttpServerChannelHandler> handlersWithOptions = new LinkedList<HttpServerChannelHandler>(); + for (HttpServerChannelHandler handler : handlers) { + String consumerMethod = handler.getConsumer().getEndpoint().getHttpMethodRestrict(); + if (consumerMethod != null && consumerMethod.contains("OPTIONS")) { + handlersWithOptions.add(handler); + } + } + return handlersWithOptions; + } + + private static HttpServerChannelHandler handlerWithTheLongestMatchingPrefix(Iterable<HttpServerChannelHandler> handlers) { + HttpServerChannelHandler handlerWithTheLongestPrefix = handlers.iterator().next(); + for (HttpServerChannelHandler handler : handlers) { + String consumerPath = handler.getConsumer().getConfiguration().getPath(); + String longestPath = handlerWithTheLongestPrefix.getConsumer().getConfiguration().getPath(); + if (consumerPath.length() > longestPath.length()) { + handlerWithTheLongestPrefix = handler; + } + } + return handlerWithTheLongestPrefix; + } + } http://git-wip-us.apache.org/repos/asf/camel/blob/517db207/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpRestContextPathMatcherTest.java ---------------------------------------------------------------------- diff --git a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpRestContextPathMatcherTest.java b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpRestContextPathMatcherTest.java new file mode 100644 index 0000000..8c068ce --- /dev/null +++ b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpRestContextPathMatcherTest.java @@ -0,0 +1,63 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.netty.http; + +import org.apache.camel.builder.RouteBuilder; +import org.junit.Test; + +import static org.apache.camel.Exchange.HTTP_METHOD; + +public class NettyHttpRestContextPathMatcherTest extends BaseNettyTest { + + @Test + public void shouldReturnCustomResponseForOptions() throws Exception { + String response = template.requestBodyAndHeader("netty-http:http://localhost:{{port}}/foo", "", HTTP_METHOD, "OPTIONS", String.class); + assertEquals("expectedOptionsResponse", response); + } + + @Test + public void shouldPreferStrictMatchOverPrefixMatch() throws Exception { + String response = template.requestBodyAndHeader("netty-http:http://localhost:{{port}}/path2/foo", "", HTTP_METHOD, "GET", String.class); + assertEquals("exact", response); + } + + @Test + public void shouldPreferOptionsForEqualPaths() throws Exception { + String response = template.requestBodyAndHeader("netty-http:http://localhost:{{port}}/path3", "", HTTP_METHOD, "POST", String.class); + assertEquals("postPath3", response); + response = template.requestBodyAndHeader("netty-http:http://localhost:{{port}}/path3", "", HTTP_METHOD, "OPTIONS", String.class); + assertEquals("optionsPath3", response); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("netty-http:http://0.0.0.0:{{port}}/path1?httpMethodRestrict=POST").setBody().constant("somePostResponse"); + from("netty-http:http://0.0.0.0:{{port}}?matchOnUriPrefix=true&httpMethodRestrict=OPTIONS").setBody().constant("expectedOptionsResponse"); + + from("netty-http:http://0.0.0.0:{{port}}/path2/foo").setBody().constant("exact"); + from("netty-http:http://0.0.0.0:{{port}}/path2?matchOnUriPrefix=true").setBody().constant("wildcard"); + + from("netty-http:http://0.0.0.0:{{port}}/path3?httpMethodRestrict=POST").setBody().constant("postPath3"); + from("netty-http:http://0.0.0.0:{{port}}/path3?httpMethodRestrict=OPTIONS").setBody().constant("optionsPath3"); + } + }; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/517db207/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/RestContextPathMatcher.java ---------------------------------------------------------------------- diff --git a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/RestContextPathMatcher.java b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/RestContextPathMatcher.java index a07435d..c7f874a 100644 --- a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/RestContextPathMatcher.java +++ b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/RestContextPathMatcher.java @@ -75,6 +75,10 @@ public class RestContextPathMatcher extends DefaultContextPathMatcher { consumerPath = consumerPath.substring(0, consumerPath.length() - 1); } + if (matchOnUriPrefix && (requestPath.startsWith(consumerPath) || consumerPath.isEmpty())) { + return true; + } + // split using single char / is optimized in the jdk String[] requestPaths = requestPath.split("/"); String[] consumerPaths = consumerPath.split("/"); http://git-wip-us.apache.org/repos/asf/camel/blob/517db207/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java ---------------------------------------------------------------------- diff --git a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java index a6027e4..5b3ad93 100644 --- a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java +++ b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java @@ -19,6 +19,7 @@ package org.apache.camel.component.netty4.http.handlers; import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -42,6 +43,7 @@ import org.apache.camel.component.netty4.http.RestContextPathMatcher; import org.apache.camel.util.UnsafeUriCharactersEncoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; @@ -181,18 +183,26 @@ public class HttpServerMultiplexChannelHandler extends SimpleChannelInboundHandl } // then see if we got a direct match - Iterator<Map.Entry<ContextPathMatcher, HttpServerChannelHandler>> it = candidates.iterator(); - while (it.hasNext()) { - Map.Entry<ContextPathMatcher, HttpServerChannelHandler> entry = it.next(); + List<HttpServerChannelHandler> directMatches = new LinkedList<HttpServerChannelHandler>(); + for (Map.Entry<ContextPathMatcher, HttpServerChannelHandler> entry : candidates) { if (entry.getKey().matchesRest(path, false)) { - answer = entry.getValue(); - break; + directMatches.add(entry.getValue()); + } + } + if (directMatches.size() == 1) { // Single match found, just return it without any further analysis. + answer = directMatches.get(0); + } else if (directMatches.size() > 1) { // possible if the prefix match occurred + List<HttpServerChannelHandler> directMatchesWithOptions = handlersWithExplicitOptionsMethod(directMatches); + if (!directMatchesWithOptions.isEmpty()) { // prefer options matches + answer = handlerWithTheLongestMatchingPrefix(directMatchesWithOptions); + } else { + answer = handlerWithTheLongestMatchingPrefix(directMatches); } } // then match by non wildcard path if (answer == null) { - it = candidates.iterator(); + Iterator<Map.Entry<ContextPathMatcher, HttpServerChannelHandler>> it = candidates.iterator(); while (it.hasNext()) { Map.Entry<ContextPathMatcher, HttpServerChannelHandler> entry = it.next(); if (!entry.getKey().matchesRest(path, true)) { @@ -239,4 +249,27 @@ public class HttpServerMultiplexChannelHandler extends SimpleChannelInboundHandl return UnsafeUriCharactersEncoder.encodeHttpURI(path); } + private static List<HttpServerChannelHandler> handlersWithExplicitOptionsMethod(Iterable<HttpServerChannelHandler> handlers) { + List<HttpServerChannelHandler> handlersWithOptions = new LinkedList<HttpServerChannelHandler>(); + for (HttpServerChannelHandler handler : handlers) { + String consumerMethod = handler.getConsumer().getEndpoint().getHttpMethodRestrict(); + if (consumerMethod != null && consumerMethod.contains("OPTIONS")) { + handlersWithOptions.add(handler); + } + } + return handlersWithOptions; + } + + private static HttpServerChannelHandler handlerWithTheLongestMatchingPrefix(Iterable<HttpServerChannelHandler> handlers) { + HttpServerChannelHandler handlerWithTheLongestPrefix = handlers.iterator().next(); + for (HttpServerChannelHandler handler : handlers) { + String consumerPath = handler.getConsumer().getConfiguration().getPath(); + String longestPath = handlerWithTheLongestPrefix.getConsumer().getConfiguration().getPath(); + if (consumerPath.length() > longestPath.length()) { + handlerWithTheLongestPrefix = handler; + } + } + return handlerWithTheLongestPrefix; + } + } http://git-wip-us.apache.org/repos/asf/camel/blob/517db207/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/NettyHttpRestContextPathMatcherTest.java ---------------------------------------------------------------------- diff --git a/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/NettyHttpRestContextPathMatcherTest.java b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/NettyHttpRestContextPathMatcherTest.java new file mode 100644 index 0000000..8fd2a7b --- /dev/null +++ b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/NettyHttpRestContextPathMatcherTest.java @@ -0,0 +1,63 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.netty4.http; + +import org.apache.camel.builder.RouteBuilder; +import org.junit.Test; + +import static org.apache.camel.Exchange.HTTP_METHOD; + +public class NettyHttpRestContextPathMatcherTest extends BaseNettyTest { + + @Test + public void shouldReturnCustomResponseForOptions() throws Exception { + String response = template.requestBodyAndHeader("netty4-http:http://localhost:{{port}}/foo", "", HTTP_METHOD, "OPTIONS", String.class); + assertEquals("expectedOptionsResponse", response); + } + + @Test + public void shouldPreferStrictMatchOverPrefixMatch() throws Exception { + String response = template.requestBodyAndHeader("netty4-http:http://localhost:{{port}}/path2/foo", "", HTTP_METHOD, "GET", String.class); + assertEquals("exact", response); + } + + @Test + public void shouldPreferOptionsForEqualPaths() throws Exception { + String response = template.requestBodyAndHeader("netty4-http:http://localhost:{{port}}/path3", "", HTTP_METHOD, "POST", String.class); + assertEquals("postPath3", response); + response = template.requestBodyAndHeader("netty4-http:http://localhost:{{port}}/path3", "", HTTP_METHOD, "OPTIONS", String.class); + assertEquals("optionsPath3", response); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("netty4-http:http://0.0.0.0:{{port}}/path1?httpMethodRestrict=POST").setBody().constant("somePostResponse"); + from("netty4-http:http://0.0.0.0:{{port}}?matchOnUriPrefix=true&httpMethodRestrict=OPTIONS").setBody().constant("expectedOptionsResponse"); + + from("netty4-http:http://0.0.0.0:{{port}}/path2/foo").setBody().constant("exact"); + from("netty4-http:http://0.0.0.0:{{port}}/path2?matchOnUriPrefix=true").setBody().constant("wildcard"); + + from("netty4-http:http://0.0.0.0:{{port}}/path3?httpMethodRestrict=POST").setBody().constant("postPath3"); + from("netty4-http:http://0.0.0.0:{{port}}/path3?httpMethodRestrict=OPTIONS").setBody().constant("optionsPath3"); + } + }; + } + +}