├── .gitignore ├── MIT-LICENSE ├── README.rst ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── publish ├── README.rst ├── build.sbt.end └── plugins.sbt.end └── src ├── main └── java │ └── io │ └── netty │ └── handler │ └── codec │ └── http │ ├── BadClientSilencer.java │ └── router │ ├── MethodlessRouter.java │ ├── OrderlessRouter.java │ ├── PathPattern.java │ ├── RouteResult.java │ ├── Router.java │ └── package-info.java └── test └── java └── io └── netty ├── example └── http │ └── router │ ├── HttpRouterServer.java │ ├── HttpRouterServerHandler.java │ └── HttpRouterServerInitializer.java └── handler └── codec └── http └── router ├── ReverseRoutingTest.java ├── RoutingTest.java └── StringRouter.java /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | project/project 3 | project/target 4 | target 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ngoc Dao 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Netty-Router is a tiny Java library intended for use with Netty 4.1, to route HTTP 2 | requests to your Netty handlers. 3 | 4 | Javadoc: 5 | 6 | * `Netty-Router `_ 7 | * `Netty `_ 8 | 9 | For usage instructions, see the Javadoc above of class ``Router`` and 10 | `the example `_. 11 | 12 | Use with Maven 13 | ~~~~~~~~~~~~~~ 14 | 15 | :: 16 | 17 | 18 | tv.cntt 19 | netty-router 20 | 2.2.0 21 | 22 | 23 | Tip: 24 | To boost Netty speed, you should also add 25 | `Javassist `_ 26 | 27 | :: 28 | 29 | 30 | org.javassist 31 | javassist 32 | 3.21.0-GA 33 | 34 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization := "tv.cntt" 2 | name := "netty-router" 3 | version := "2.2.0-SNAPSHOT" 4 | 5 | //------------------------------------------------------------------------------ 6 | 7 | // This project does not use Scala, only SBT as build tool 8 | autoScalaLibrary := false 9 | 10 | // Do not append Scala versions to the generated artifacts 11 | crossPaths := false 12 | 13 | // Netty 4+ requires Java 6 14 | javacOptions in Compile ++= Seq("-source", "1.6", "-target", "1.6", "-Xlint:deprecation") 15 | 16 | javacOptions in (Compile, doc) := Seq("-source", "1.6") 17 | 18 | //------------------------------------------------------------------------------ 19 | 20 | libraryDependencies += "io.netty" % "netty-all" % "4.1.11.Final" % "provided" 21 | libraryDependencies += "junit" % "junit" % "4.12" % "test" 22 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.15 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Run sbt eclipse to create Eclipse project file 2 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.1.0") 3 | -------------------------------------------------------------------------------- /publish/README.rst: -------------------------------------------------------------------------------- 1 | Publish to local 2 | ---------------- 3 | 4 | While developing, you may need do local publish. Run 5 | ``sbt publish-local``. 6 | 7 | To delete the local publish: 8 | 9 | :: 10 | 11 | $ find ~/.ivy2 -name *netty-router* -delete 12 | 13 | Publish to Sonatype 14 | ------------------- 15 | 16 | See: 17 | https://github.com/sbt/sbt.github.com/blob/gen-master/src/jekyll/using_sonatype.md 18 | 19 | Create ~/.sbt/0.13/sonatype.sbt (for SBT 0.12: ~/.sbt/sonatype.sbt) file: 20 | 21 | :: 22 | 23 | credentials += Credentials("Sonatype Nexus Repository Manager", 24 | "oss.sonatype.org", 25 | "", 26 | "") 27 | 28 | Then: 29 | 30 | 1. Copy content of 31 | publish/build.sbt.end to the end of build.sbt 32 | publish/plugins.sbt.end to the end of project/plugins.sbt 33 | 2. Run ``sbt publish-signed``. Alternatively you can run ``sbt`` then from SBT 34 | command prompt run ``+ publish-signed``. 35 | 3. Login at https://oss.sonatype.org/ and from "Staging Repositories" select the 36 | newly published item, click "Close" then "Release". 37 | -------------------------------------------------------------------------------- /publish/build.sbt.end: -------------------------------------------------------------------------------- 1 | 2 | // Add diagrams to Scaladoc, graphviz is required 3 | scalacOptions in (Compile, doc) += "-diagrams" 4 | 5 | // Publish to Sonatype 6 | // https://github.com/sbt/sbt.github.com/blob/gen-master/src/jekyll/using_sonatype.md 7 | 8 | publishTo <<= (version) { version: String => 9 | val nexus = "https://oss.sonatype.org/" 10 | if (version.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") 11 | else Some("releases" at nexus + "service/local/staging/deploy/maven2") 12 | } 13 | 14 | publishMavenStyle := true 15 | 16 | publishArtifact in Test := false 17 | 18 | pomIncludeRepository := { _ => false } 19 | 20 | pomExtra := ( 21 | https://github.com/sinetja/netty-router 22 | 23 | 24 | MIT 25 | https://github.com/sinetja/netty-router/blob/master/MIT-LICENSE 26 | repo 27 | 28 | 29 | 30 | git@github.com:sinetja/netty-router.git 31 | scm:git:git@github.com:sinetja/netty-router.git 32 | 33 | 34 | 35 | ngocdaothanh 36 | Ngoc Dao 37 | https://github.com/ngocdaothanh 38 | 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /publish/plugins.sbt.end: -------------------------------------------------------------------------------- 1 | 2 | // http://www.cakesolutions.net/teamblogs/2012/01/28/publishing-sbt-projects-to-nexus/ 3 | // https://groups.google.com/forum/?fromgroups=#!topic/simple-build-tool/wuiMh-Ue_iw 4 | addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3") 5 | -------------------------------------------------------------------------------- /src/main/java/io/netty/handler/codec/http/BadClientSilencer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http; 17 | 18 | import io.netty.channel.ChannelHandler.Sharable; 19 | import io.netty.channel.ChannelHandlerContext; 20 | import io.netty.channel.SimpleChannelInboundHandler; 21 | import io.netty.util.internal.logging.InternalLogger; 22 | import io.netty.util.internal.logging.InternalLoggerFactory; 23 | 24 | /** 25 | * This utility handler should be put at the last position of the inbound pipeline to 26 | * catch all exceptions caused by bad client (closed connection, malformed request etc.) 27 | * and server processing, then close the connection. 28 | * 29 | * By default exceptions are logged to Netty internal logger. You may need to override 30 | * {@link #onUnknownMessage(Object)}, {@link #onBadClient(Throwable)}, and 31 | * {@link #onBadServer(Throwable)} to log to more suitable places. 32 | */ 33 | @Sharable 34 | public class BadClientSilencer extends SimpleChannelInboundHandler { 35 | private static final InternalLogger log = InternalLoggerFactory.getInstance(BadClientSilencer.class); 36 | 37 | /** Logs to Netty internal logger. Override this method to log to other places if you want. */ 38 | protected void onUnknownMessage(Object msg) { 39 | log.warn("Unknown msg: " + msg); 40 | } 41 | 42 | /** Logs to Netty internal logger. Override this method to log to other places if you want. */ 43 | protected void onBadClient(Throwable e) { 44 | log.warn("Caught exception (maybe client is bad)", e); 45 | } 46 | 47 | /** Logs to Netty internal logger. Override this method to log to other places if you want. */ 48 | protected void onBadServer(Throwable e) { 49 | log.warn("Caught exception (maybe server is bad)", e); 50 | } 51 | 52 | //---------------------------------------------------------------------------- 53 | 54 | @Override 55 | public void channelRead0(ChannelHandlerContext ctx, Object msg) { 56 | // This handler is the last inbound handler. 57 | // This means msg has not been handled by any previous handler. 58 | ctx.close(); 59 | 60 | if (msg != LastHttpContent.EMPTY_LAST_CONTENT) { 61 | onUnknownMessage(msg); 62 | } 63 | } 64 | 65 | @Override 66 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) { 67 | ctx.close(); 68 | 69 | // To clarify where exceptions are from, imports are not used 70 | if (e instanceof java.io.IOException || // Connection reset by peer, Broken pipe 71 | e instanceof java.nio.channels.ClosedChannelException || 72 | e instanceof io.netty.handler.codec.DecoderException || 73 | e instanceof io.netty.handler.codec.CorruptedFrameException || // Bad WebSocket frame 74 | e instanceof java.lang.IllegalArgumentException || // Use https://... to connect to HTTP server 75 | e instanceof javax.net.ssl.SSLException || // Use http://... to connect to HTTPS server 76 | e instanceof io.netty.handler.ssl.NotSslRecordException) { 77 | onBadClient(e); // Maybe client is bad 78 | } else { 79 | onBadServer(e); // Maybe server is bad 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/io/netty/handler/codec/http/router/MethodlessRouter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http.router; 17 | 18 | /** 19 | * Router that contains information about route matching orders, but doesn't 20 | * contain information about HTTP request methods. 21 | * 22 | *

Routes are devided into 3 sections: "first", "last", and "other". 23 | * Routes in "first" are matched first, then in "other", then in "last". 24 | */ 25 | final class MethodlessRouter { 26 | private final OrderlessRouter first = new OrderlessRouter(); 27 | private final OrderlessRouter other = new OrderlessRouter(); 28 | private final OrderlessRouter last = new OrderlessRouter(); 29 | 30 | //-------------------------------------------------------------------------- 31 | 32 | /** 33 | * Returns the "first" router; routes in this router will be matched first. 34 | */ 35 | public OrderlessRouter first() { 36 | return first; 37 | } 38 | 39 | /** 40 | * Returns the "other" router; routes in this router will be matched after 41 | * those in the "first" router, but before those in the "last" router. 42 | */ 43 | public OrderlessRouter other() { 44 | return other; 45 | } 46 | 47 | /** 48 | * Returns the "last" router; routes in this router will be matched last. 49 | */ 50 | public OrderlessRouter last() { 51 | return last; 52 | } 53 | 54 | /** 55 | * Returns the number of routes in this router. 56 | */ 57 | public int size() { 58 | return first.routes().size() + other.routes().size() + last.routes().size(); 59 | } 60 | 61 | //-------------------------------------------------------------------------- 62 | 63 | /** 64 | * Adds route to the "first" section. 65 | * 66 | *

A path pattern can only point to one target. This method does nothing if the pattern 67 | * has already been added. 68 | */ 69 | public MethodlessRouter addRouteFirst(String pathPattern, T target) { 70 | first.addRoute(pathPattern, target); 71 | return this; 72 | } 73 | 74 | /** 75 | * Adds route to the "other" section. 76 | * 77 | *

A path pattern can only point to one target. This method does nothing if the pattern 78 | * has already been added. 79 | */ 80 | public MethodlessRouter addRoute(String pathPattern, T target) { 81 | other.addRoute(pathPattern, target); 82 | return this; 83 | } 84 | 85 | /** 86 | * Adds route to the "last" section. 87 | * 88 | *

A path pattern can only point to one target. This method does nothing if the pattern 89 | * has already been added. 90 | */ 91 | public MethodlessRouter addRouteLast(String pathPattern, T target) { 92 | last.addRoute(pathPattern, target); 93 | return this; 94 | } 95 | 96 | //-------------------------------------------------------------------------- 97 | 98 | /** 99 | * Removes the route specified by the path pattern. 100 | */ 101 | public void removePathPattern(String pathPattern) { 102 | first.removePathPattern(pathPattern); 103 | other.removePathPattern(pathPattern); 104 | last.removePathPattern(pathPattern); 105 | } 106 | 107 | /** 108 | * Removes all routes leading to the target. 109 | */ 110 | public void removeTarget(T target) { 111 | first.removeTarget(target); 112 | other.removeTarget(target); 113 | last.removeTarget(target); 114 | } 115 | 116 | //-------------------------------------------------------------------------- 117 | 118 | /** 119 | * @return {@code null} if no match 120 | */ 121 | public RouteResult route(String uri, String decodedPath, String[] pathTokens) { 122 | RouteResult ret = first.route(uri, decodedPath, pathTokens); 123 | if (ret != null) { 124 | return ret; 125 | } 126 | 127 | ret = other.route(uri, decodedPath, pathTokens); 128 | if (ret != null) { 129 | return ret; 130 | } 131 | 132 | ret = last.route(uri, decodedPath, pathTokens); 133 | if (ret != null) { 134 | return ret; 135 | } 136 | 137 | return null; 138 | } 139 | 140 | /** 141 | * Checks if there's any matching route. 142 | */ 143 | public boolean anyMatched(String[] requestPathTokens) { 144 | return first.anyMatched(requestPathTokens) || 145 | other.anyMatched(requestPathTokens) || 146 | last.anyMatched(requestPathTokens); 147 | } 148 | 149 | /** 150 | * Given a target and params, this method tries to do the reverse routing 151 | * and returns the URI. 152 | * 153 | *

Placeholders in the path pattern will be filled with the params. 154 | * The params can be a map of {@code placeholder name -> value} 155 | * or ordered values. 156 | * 157 | *

If a param doesn't have a corresponding placeholder, it will be put 158 | * to the query part of the result URI. 159 | * 160 | * @return {@code null} if there's no match 161 | */ 162 | public String uri(T target, Object... params) { 163 | String ret = first.uri(target, params); 164 | if (ret != null) { 165 | return ret; 166 | } 167 | 168 | ret = other.uri(target, params); 169 | if (ret != null) { 170 | return ret; 171 | } 172 | 173 | return last.uri(target, params); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/io/netty/handler/codec/http/router/OrderlessRouter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http.router; 17 | 18 | import io.netty.util.internal.ObjectUtil; 19 | import io.netty.util.internal.logging.InternalLogger; 20 | import io.netty.util.internal.logging.InternalLoggerFactory; 21 | 22 | import java.io.UnsupportedEncodingException; 23 | import java.net.URLEncoder; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Set; 30 | 31 | /** 32 | * Router that doesn't contain information about HTTP request methods and route 33 | * matching orders. 34 | */ 35 | final class OrderlessRouter { 36 | private static final InternalLogger log = InternalLoggerFactory.getInstance(OrderlessRouter.class); 37 | 38 | // A path pattern can only point to one target 39 | private final Map routes = new HashMap(); 40 | 41 | // Reverse index to create reverse routes fast (a target can have multiple path patterns) 42 | private final Map> reverseRoutes = new HashMap>(); 43 | 44 | //-------------------------------------------------------------------------- 45 | 46 | /** 47 | * Returns all routes in this router, an unmodifiable map of {@code PathPattern -> Target}. 48 | */ 49 | public Map routes() { 50 | return Collections.unmodifiableMap(routes); 51 | } 52 | 53 | /** 54 | * This method does nothing if the path pattern has already been added. 55 | * A path pattern can only point to one target. 56 | */ 57 | public OrderlessRouter addRoute(String pathPattern, T target) { 58 | PathPattern p = new PathPattern(pathPattern); 59 | if (routes.containsKey(p)) { 60 | return this; 61 | } 62 | 63 | routes.put(p, target); 64 | addReverseRoute(target, p); 65 | return this; 66 | } 67 | 68 | private void addReverseRoute(T target, PathPattern pathPattern) { 69 | Set patterns = reverseRoutes.get(target); 70 | if (patterns == null) { 71 | patterns = new HashSet(); 72 | patterns.add(pathPattern); 73 | reverseRoutes.put(target, patterns); 74 | } else { 75 | patterns.add(pathPattern); 76 | } 77 | } 78 | 79 | //-------------------------------------------------------------------------- 80 | 81 | /** 82 | * Removes the route specified by the path pattern. 83 | */ 84 | public void removePathPattern(String pathPattern) { 85 | PathPattern p = new PathPattern(pathPattern); 86 | T target = routes.remove(p); 87 | if (target == null) { 88 | return; 89 | } 90 | 91 | Set paths = reverseRoutes.remove(target); 92 | paths.remove(p); 93 | } 94 | 95 | /** 96 | * Removes all routes leading to the target. 97 | */ 98 | public void removeTarget(T target) { 99 | Set patterns = reverseRoutes.remove(ObjectUtil.checkNotNull(target, "target")); 100 | if (patterns == null) { 101 | return; 102 | } 103 | 104 | // A pattern can only point to one target. 105 | // A target can have multiple patterns. 106 | // Remove all patterns leading to this target. 107 | for (PathPattern pattern : patterns) { 108 | routes.remove(pattern); 109 | } 110 | } 111 | 112 | //-------------------------------------------------------------------------- 113 | 114 | /** 115 | * @return {@code null} if no match 116 | */ 117 | public RouteResult route(String uri, String decodedPath, String[] pathTokens) { 118 | // Optimize: reuse requestPathTokens and pathParams in the loop 119 | Map pathParams = new HashMap(); 120 | for (Map.Entry entry : routes.entrySet()) { 121 | PathPattern pattern = entry.getKey(); 122 | if (pattern.match(pathTokens, pathParams)) { 123 | T target = entry.getValue(); 124 | return new RouteResult(uri, decodedPath, pathParams, Collections.>emptyMap(), target); 125 | } 126 | 127 | // Reset for the next try 128 | pathParams.clear(); 129 | } 130 | 131 | return null; 132 | } 133 | 134 | /** 135 | * Checks if there's any matching route. 136 | */ 137 | public boolean anyMatched(String[] requestPathTokens) { 138 | Map pathParams = new HashMap(); 139 | for (PathPattern pattern : routes.keySet()) { 140 | if (pattern.match(requestPathTokens, pathParams)) { 141 | return true; 142 | } 143 | 144 | // Reset for the next loop 145 | pathParams.clear(); 146 | } 147 | 148 | return false; 149 | } 150 | 151 | //-------------------------------------------------------------------------- 152 | 153 | /** 154 | * Given a target and params, this method tries to do the reverse routing 155 | * and returns the URI. 156 | * 157 | *

Placeholders in the path pattern will be filled with the params. 158 | * The params can be a map of {@code placeholder name -> value} 159 | * or ordered values. 160 | * 161 | *

If a param doesn't have a corresponding placeholder, it will be put 162 | * to the query part of the result URI. 163 | * 164 | * @return {@code null} if there's no match 165 | */ 166 | @SuppressWarnings("unchecked") 167 | public String uri(T target, Object... params) { 168 | if (params.length == 0) { 169 | return uri(target, Collections.emptyMap()); 170 | } 171 | 172 | if (params.length == 1 && params[0] instanceof Map) { 173 | return pathMap(target, (Map) params[0]); 174 | } 175 | 176 | if (params.length % 2 == 1) { 177 | throw new IllegalArgumentException("Missing value for param: " + params[params.length - 1]); 178 | } 179 | 180 | Map map = new HashMap(params.length / 2); 181 | for (int i = 0; i < params.length; i += 2) { 182 | String key = params[i].toString(); 183 | String value = params[i + 1].toString(); 184 | map.put(key, value); 185 | } 186 | return pathMap(target, map); 187 | } 188 | 189 | /** 190 | * @return {@code null} if there's no match, or the params can't be UTF-8 encoded 191 | */ 192 | private String pathMap(T target, Map params) { 193 | Set patterns = reverseRoutes.get(target); 194 | if (patterns == null) { 195 | return null; 196 | } 197 | 198 | try { 199 | // The best one is the one with minimum number of params in the query 200 | String bestCandidate = null; 201 | int minQueryParams = Integer.MAX_VALUE; 202 | 203 | boolean matched = true; 204 | Set usedKeys = new HashSet(); 205 | 206 | for (PathPattern pattern : patterns) { 207 | matched = true; 208 | usedKeys.clear(); 209 | 210 | // "+ 16": Just in case the part befor that is 0 211 | int initialCapacity = pattern.pattern().length() + 20 * params.size() + 16; 212 | StringBuilder b = new StringBuilder(initialCapacity); 213 | 214 | for (String token : pattern.tokens()) { 215 | b.append('/'); 216 | 217 | if (token.length() > 0 && token.charAt(0) == ':') { 218 | String key = token.substring(1); 219 | Object value = params.get(key); 220 | if (value == null) { 221 | matched = false; 222 | break; 223 | } 224 | 225 | usedKeys.add(key); 226 | b.append(value.toString()); 227 | } else { 228 | b.append(token); 229 | } 230 | } 231 | 232 | if (matched) { 233 | int numQueryParams = params.size() - usedKeys.size(); 234 | if (numQueryParams < minQueryParams) { 235 | if (numQueryParams > 0) { 236 | boolean firstQueryParam = true; 237 | 238 | for (Map.Entry entry : params.entrySet()) { 239 | String key = entry.getKey().toString(); 240 | if (!usedKeys.contains(key)) { 241 | if (firstQueryParam) { 242 | b.append('?'); 243 | firstQueryParam = false; 244 | } else { 245 | b.append('&'); 246 | } 247 | 248 | String value = entry.getValue().toString(); 249 | 250 | // May throw UnsupportedEncodingException 251 | b.append(URLEncoder.encode(key, "UTF-8")); 252 | 253 | b.append('='); 254 | 255 | // May throw UnsupportedEncodingException 256 | b.append(URLEncoder.encode(value, "UTF-8")); 257 | } 258 | } 259 | } 260 | 261 | bestCandidate = b.toString(); 262 | minQueryParams = numQueryParams; 263 | } 264 | } 265 | } 266 | 267 | return bestCandidate; 268 | } catch (UnsupportedEncodingException e) { 269 | log.warn("Params can't be UTF-8 encoded: " + params); 270 | return null; 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/main/java/io/netty/handler/codec/http/router/PathPattern.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http.router; 17 | 18 | import io.netty.util.internal.ObjectUtil; 19 | 20 | import java.util.Map; 21 | 22 | /** 23 | * The pattern can contain constants or placeholders, example: 24 | * {@code constant1/:placeholder1/constant2/:*}. 25 | * 26 | *

{@code :*} is a special placeholder to catch the rest of the path 27 | * (may include slashes). If exists, it must appear at the end of the path. 28 | * 29 | *

The pattern must not contain query, example: 30 | * {@code constant1/constant2?foo=bar}. 31 | * 32 | *

The pattern will be broken to tokens, example: 33 | * {@code ["constant1", ":variable", "constant2", ":*"]} 34 | */ 35 | final class PathPattern { 36 | public static String removeSlashesAtBothEnds(String path) { 37 | ObjectUtil.checkNotNull(path, "path"); 38 | 39 | if (path.isEmpty()) { 40 | return path; 41 | } 42 | 43 | int beginIndex = 0; 44 | while (beginIndex < path.length() && path.charAt(beginIndex) == '/') { 45 | beginIndex++; 46 | } 47 | if (beginIndex == path.length()) { 48 | return ""; 49 | } 50 | 51 | int endIndex = path.length() - 1; 52 | while (endIndex > beginIndex && path.charAt(endIndex) == '/') { 53 | endIndex--; 54 | } 55 | 56 | return path.substring(beginIndex, endIndex + 1); 57 | } 58 | 59 | //-------------------------------------------------------------------------- 60 | 61 | private final String pattern; 62 | private final String[] tokens; 63 | 64 | /** 65 | * The pattern must not contain query, example: 66 | * {@code constant1/constant2?foo=bar}. 67 | * 68 | *

The pattern will be stored without slashes at both ends. 69 | */ 70 | public PathPattern(String pattern) { 71 | if (pattern.contains("?")) { 72 | throw new IllegalArgumentException("Path pattern must not contain query"); 73 | } 74 | 75 | this.pattern = removeSlashesAtBothEnds(ObjectUtil.checkNotNull(pattern, "pattern")); 76 | this.tokens = this.pattern.split("/"); 77 | } 78 | 79 | /** 80 | * Returns the pattern given at the constructor, without slashes at both ends. 81 | */ 82 | public String pattern() { 83 | return pattern; 84 | } 85 | 86 | /** 87 | * Returns the pattern given at the constructor, without slashes at both ends, 88 | * and split by {@code '/'}. 89 | */ 90 | public String[] tokens() { 91 | return tokens; 92 | } 93 | 94 | //-------------------------------------------------------------------------- 95 | // Instances of this class can be conveniently used as Map keys. 96 | 97 | @Override 98 | public int hashCode() { 99 | return pattern.hashCode(); 100 | } 101 | 102 | @Override 103 | public boolean equals(Object o) { 104 | if (this == o) { 105 | return true; 106 | } 107 | 108 | if (!(o instanceof PathPattern)) { 109 | return false; 110 | } 111 | 112 | PathPattern otherPathPattern = (PathPattern) o; 113 | return pattern.equals(otherPathPattern.pattern); 114 | } 115 | 116 | //-------------------------------------------------------------------------- 117 | 118 | /** 119 | * {@code params} will be updated with params embedded in the request path. 120 | * 121 | *

This method signature is designed so that {@code requestPathTokens} and {@code params} 122 | * can be created only once then reused, to optimize for performance when a 123 | * large number of path patterns need to be matched. 124 | * 125 | * @return {@code false} if not matched; in this case params should be reset 126 | */ 127 | public boolean match(String[] requestPathTokens, Map params) { 128 | if (tokens.length == requestPathTokens.length) { 129 | for (int i = 0; i < tokens.length; i++) { 130 | String key = tokens[i]; 131 | String value = requestPathTokens[i]; 132 | 133 | if (key.length() > 0 && key.charAt(0) == ':') { 134 | // This is a placeholder 135 | params.put(key.substring(1), value); 136 | } else if (!key.equals(value)) { 137 | // This is a constant 138 | return false; 139 | } 140 | } 141 | 142 | return true; 143 | } 144 | 145 | if (tokens.length > 0 && 146 | tokens[tokens.length - 1].equals(":*") && 147 | tokens.length <= requestPathTokens.length) { 148 | // The first part 149 | for (int i = 0; i < tokens.length - 2; i++) { 150 | String key = tokens[i]; 151 | String value = requestPathTokens[i]; 152 | 153 | if (key.length() > 0 && key.charAt(0) == ':') { 154 | // This is a placeholder 155 | params.put(key.substring(1), value); 156 | } else if (!key.equals(value)) { 157 | // This is a constant 158 | return false; 159 | } 160 | } 161 | 162 | // The last :* part 163 | StringBuilder b = new StringBuilder(requestPathTokens[tokens.length - 1]); 164 | for (int i = tokens.length; i < requestPathTokens.length; i++) { 165 | b.append('/'); 166 | b.append(requestPathTokens[i]); 167 | } 168 | params.put("*", b.toString()); 169 | 170 | return true; 171 | } 172 | 173 | return false; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/io/netty/handler/codec/http/router/RouteResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http.router; 17 | 18 | import io.netty.handler.codec.http.HttpMethod; 19 | import io.netty.util.internal.ObjectUtil; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | /** 27 | * Result of calling {@link Router#route(HttpMethod, String)}. 28 | */ 29 | public class RouteResult { 30 | private final String uri; 31 | private final String decodedPath; 32 | 33 | private final Map pathParams; 34 | private final Map> queryParams; 35 | 36 | private final T target; 37 | 38 | /** 39 | * The maps will be wrapped in Collections.unmodifiableMap. 40 | */ 41 | public RouteResult( 42 | String uri, String decodedPath, 43 | Map pathParams, Map> queryParams, 44 | T target 45 | ) { 46 | this.uri = ObjectUtil.checkNotNull(uri, "uri"); 47 | this.decodedPath = ObjectUtil.checkNotNull(decodedPath, "decodedPath"); 48 | this.pathParams = Collections.unmodifiableMap(ObjectUtil.checkNotNull(pathParams, "pathParams")); 49 | this.queryParams = Collections.unmodifiableMap(ObjectUtil.checkNotNull(queryParams, "queryParams")); 50 | this.target = ObjectUtil.checkNotNull(target, "target"); 51 | } 52 | 53 | /** 54 | * Returns the original request URI. 55 | */ 56 | public String uri() { 57 | return uri; 58 | } 59 | 60 | /** 61 | * Returns the decoded request path. 62 | */ 63 | public String decodedPath() { 64 | return decodedPath; 65 | } 66 | 67 | /** 68 | * Returns all params embedded in the request path. 69 | */ 70 | public Map pathParams() { 71 | return pathParams; 72 | } 73 | 74 | /** 75 | * Returns all params in the query part of the request URI. 76 | */ 77 | public Map> queryParams() { 78 | return queryParams; 79 | } 80 | 81 | public T target() { 82 | return target; 83 | } 84 | 85 | //---------------------------------------------------------------------------- 86 | // Utilities to get params. 87 | 88 | /** 89 | * Extracts the first matching param in {@code queryParams}. 90 | * 91 | * @return {@code null} if there's no match 92 | */ 93 | public String queryParam(String name) { 94 | List values = queryParams.get(name); 95 | return (values == null) ? null : values.get(0); 96 | } 97 | 98 | /** 99 | * Extracts the param in {@code pathParams} first, then falls back to the first matching 100 | * param in {@code queryParams}. 101 | * 102 | * @return {@code null} if there's no match 103 | */ 104 | public String param(String name) { 105 | String pathValue = pathParams.get(name); 106 | return (pathValue == null) ? queryParam(name) : pathValue; 107 | } 108 | 109 | /** 110 | * Extracts all params in {@code pathParams} and {@code queryParams} matching the name. 111 | * 112 | * @return Unmodifiable list; the list is empty if there's no match 113 | */ 114 | public List params(String name) { 115 | List values = queryParams.get(name); 116 | String value = pathParams.get(name); 117 | 118 | if (values == null) { 119 | return (value == null) ? Collections.emptyList() : Collections.singletonList(value); 120 | } 121 | 122 | if (value == null) { 123 | return Collections.unmodifiableList(values); 124 | } else { 125 | List aggregated = new ArrayList(values.size() + 1); 126 | aggregated.addAll(values); 127 | aggregated.add(value); 128 | return Collections.unmodifiableList(aggregated); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/io/netty/handler/codec/http/router/Router.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http.router; 17 | 18 | import io.netty.handler.codec.http.HttpMethod; 19 | import io.netty.handler.codec.http.QueryStringDecoder; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Collection; 23 | import java.util.Collections; 24 | import java.util.HashMap; 25 | import java.util.HashSet; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.Map.Entry; 29 | import java.util.Set; 30 | 31 | /** 32 | * Router that contains information about both route matching orders and 33 | * HTTP request methods. 34 | * 35 | *

Routes are devided into 3 sections: "first", "last", and "other". 36 | * Routes in "first" are matched first, then in "other", then in "last". 37 | * 38 | *

Create router

39 | * 40 | *

Route targets can be any type. In the below example, targets are classes: 41 | * 42 | *

 43 |  * {@code
 44 |  * Router router = new Router()
 45 |  *   .GET      ("/articles",     IndexHandler.class)
 46 |  *   .GET      ("/articles/:id", ShowHandler.class)
 47 |  *   .POST     ("/articles",     CreateHandler.class)
 48 |  *   .GET      ("/download/:*",  DownloadHandler.class)  // ":*" must be the last token
 49 |  *   .GET_FIRST("/articles/new", NewHandler.class);      // This will be matched first
 50 |  * }
 51 |  * 
52 | * 53 | *

Slashes at both ends are ignored. These are the same: 54 | * 55 | *

 56 |  * {@code
 57 |  * router.GET("articles",   IndexHandler.class);
 58 |  * router.GET("/articles",  IndexHandler.class);
 59 |  * router.GET("/articles/", IndexHandler.class);
 60 |  * }
 61 |  * 
62 | * 63 | *

You can remove routes by target or by path pattern: 64 | * 65 | *

 66 |  * {@code
 67 |  * router.removeTarget(IndexHandler.class);
 68 |  * router.removePathPattern("/articles");
 69 |  * }
 70 |  * 
71 | * 72 | *

Match with request method and URI

73 | * 74 | *

Use {@link #route(HttpMethod, String)}. 75 | * 76 | *

From the {@link RouteResult} you can extract params embedded in 77 | * the path and from the query part of the request URI. 78 | * 79 | *

404 Not Found target

80 | * 81 | *

Use {@link #notFound(Object)}. It will be used as the target 82 | * when there's no match. 83 | * 84 | *

 85 |  * {@code
 86 |  * router.notFound(My404Handler.class);
 87 |  * }
 88 |  * 
89 | * 90 | *

Create reverse route

91 | * 92 | *

Use {@code #uri}: 93 | * 94 | *

 95 |  * {@code
 96 |  * router.uri(HttpMethod.GET, IndexHandler.class);
 97 |  * // Returns "/articles"
 98 |  * }
 99 |  * 
100 | * 101 | *

You can skip HTTP method if there's no confusion: 102 | * 103 | *

104 |  * {@code
105 |  * router.uri(CreateHandler.class);
106 |  * // Also returns "/articles"
107 |  * }
108 |  * 
109 | * 110 | *

You can specify params as map: 111 | * 112 | *

113 |  * {@code
114 |  * // Things in params will be converted to String
115 |  * Map params = new HashMap();
116 |  * params.put("id", 123);
117 |  * router.uri(ShowHandler.class, params);
118 |  * // Returns "/articles/123"
119 |  * }
120 |  * 
121 | * 122 | *

Convenient way to specify params: 123 | * 124 | *

125 |  * {@code
126 |  * router.uri(ShowHandler.class, "id", 123);
127 |  * // Returns "/articles/123"
128 |  * }
129 |  * 
130 | * 131 | *

Allowed methods

132 | * 133 | *

If you want to implement 134 | * OPTIONS 135 | * or 136 | * CORS, 137 | * you can use {@link #allowedMethods(String)}. 138 | * 139 | *

For {@code OPTIONS *}, use {@link #allAllowedMethods()}. 140 | */ 141 | public class Router { 142 | private final Map> routers = 143 | new HashMap>(); 144 | 145 | private final MethodlessRouter anyMethodRouter = 146 | new MethodlessRouter(); 147 | 148 | private T notFound; 149 | 150 | //-------------------------------------------------------------------------- 151 | // Design decision: 152 | // We do not allow access to routers and anyMethodRouter, because we don't 153 | // want to expose MethodlessRouter, OrderlessRouter, and PathPattern. 154 | // Exposing those will complicate the use of this package. 155 | 156 | /** 157 | * Returns the fallback target for use when there's no match at 158 | * {@link #route(HttpMethod, String)}. 159 | */ 160 | public T notFound() { 161 | return notFound; 162 | } 163 | 164 | /** 165 | * Returns the number of routes in this router. 166 | */ 167 | public int size() { 168 | int ret = anyMethodRouter.size(); 169 | 170 | for (MethodlessRouter router : routers.values()) { 171 | ret += router.size(); 172 | } 173 | 174 | return ret; 175 | } 176 | 177 | //-------------------------------------------------------------------------- 178 | 179 | /** 180 | * Add route to the "first" section. 181 | * 182 | *

A path pattern can only point to one target. This method does nothing if the pattern 183 | * has already been added. 184 | */ 185 | public Router addRouteFirst(HttpMethod method, String pathPattern, T target) { 186 | getMethodlessRouter(method).addRouteFirst(pathPattern, target); 187 | return this; 188 | } 189 | 190 | /** 191 | * Add route to the "other" section. 192 | * 193 | *

A path pattern can only point to one target. This method does nothing if the pattern 194 | * has already been added. 195 | */ 196 | public Router addRoute(HttpMethod method, String pathPattern, T target) { 197 | getMethodlessRouter(method).addRoute(pathPattern, target); 198 | return this; 199 | } 200 | 201 | /** 202 | * Add route to the "last" section. 203 | * 204 | *

A path pattern can only point to one target. This method does nothing if the pattern 205 | * has already been added. 206 | */ 207 | public Router addRouteLast(HttpMethod method, String pathPattern, T target) { 208 | getMethodlessRouter(method).addRouteLast(pathPattern, target); 209 | return this; 210 | } 211 | 212 | /** 213 | * Sets the fallback target for use when there's no match at 214 | * {@link #route(HttpMethod, String)}. 215 | */ 216 | public Router notFound(T target) { 217 | this.notFound = target; 218 | return this; 219 | } 220 | 221 | private MethodlessRouter getMethodlessRouter(HttpMethod method) { 222 | if (method == null) { 223 | return anyMethodRouter; 224 | } 225 | 226 | MethodlessRouter r = routers.get(method); 227 | if (r == null) { 228 | r = new MethodlessRouter(); 229 | routers.put(method, r); 230 | } 231 | 232 | return r; 233 | } 234 | 235 | //-------------------------------------------------------------------------- 236 | 237 | /** 238 | * Removes the route specified by the path pattern. 239 | */ 240 | public void removePathPattern(String pathPattern) { 241 | for (MethodlessRouter r : routers.values()) { 242 | r.removePathPattern(pathPattern); 243 | } 244 | anyMethodRouter.removePathPattern(pathPattern); 245 | } 246 | 247 | /** 248 | * Removes all routes leading to the target. 249 | */ 250 | public void removeTarget(T target) { 251 | for (MethodlessRouter r : routers.values()) { 252 | r.removeTarget(target); 253 | } 254 | anyMethodRouter.removeTarget(target); 255 | } 256 | 257 | //-------------------------------------------------------------------------- 258 | 259 | /** 260 | * If there's no match, returns the result with {@link #notFound(Object) notFound} 261 | * as the target if it is set, otherwise returns {@code null}. 262 | */ 263 | public RouteResult route(HttpMethod method, String uri) { 264 | MethodlessRouter router = routers.get(method); 265 | if (router == null) { 266 | router = anyMethodRouter; 267 | } 268 | 269 | QueryStringDecoder decoder = new QueryStringDecoder(uri); 270 | String[] tokens = decodePathTokens(uri); 271 | 272 | RouteResult ret = router.route(uri, decoder.path(), tokens); 273 | if (ret != null) { 274 | return new RouteResult(uri, decoder.path(), ret.pathParams(), decoder.parameters(), ret.target()); 275 | } 276 | 277 | if (router != anyMethodRouter) { 278 | ret = anyMethodRouter.route(uri, decoder.path(), tokens); 279 | if (ret != null) { 280 | return new RouteResult(uri, decoder.path(), ret.pathParams(), decoder.parameters(), ret.target()); 281 | } 282 | } 283 | 284 | if (notFound != null) { 285 | return new RouteResult(uri, decoder.path(), Collections.emptyMap(), decoder.parameters(), notFound); 286 | } 287 | 288 | return null; 289 | } 290 | 291 | private String[] decodePathTokens(String uri) { 292 | // Need to split the original URI (instead of QueryStringDecoder#path) then decode the tokens (components), 293 | // otherwise /test1/123%2F456 will not match /test1/:p1 294 | 295 | int qPos = uri.indexOf("?"); 296 | String encodedPath = (qPos >= 0) ? uri.substring(0, qPos) : uri; 297 | 298 | String[] encodedTokens = PathPattern.removeSlashesAtBothEnds(encodedPath).split("/"); 299 | 300 | String[] decodedTokens = new String[encodedTokens.length]; 301 | for (int i = 0; i < encodedTokens.length; i++) { 302 | String encodedToken = encodedTokens[i]; 303 | decodedTokens[i] = QueryStringDecoder.decodeComponent(encodedToken); 304 | } 305 | 306 | return decodedTokens; 307 | } 308 | 309 | //-------------------------------------------------------------------------- 310 | // For implementing OPTIONS and CORS. 311 | 312 | /** 313 | * Returns allowed methods for a specific URI. 314 | *

315 | * For {@code OPTIONS *}, use {@link #allAllowedMethods()} instead of this method. 316 | */ 317 | public Set allowedMethods(String uri) { 318 | QueryStringDecoder decoder = new QueryStringDecoder(uri); 319 | String[] tokens = PathPattern.removeSlashesAtBothEnds(decoder.path()).split("/"); 320 | 321 | if (anyMethodRouter.anyMatched(tokens)) { 322 | return allAllowedMethods(); 323 | } 324 | 325 | Set ret = new HashSet(routers.size()); 326 | for (Map.Entry> entry : routers.entrySet()) { 327 | MethodlessRouter router = entry.getValue(); 328 | if (router.anyMatched(tokens)) { 329 | HttpMethod method = entry.getKey(); 330 | ret.add(method); 331 | } 332 | } 333 | 334 | return ret; 335 | } 336 | 337 | /** 338 | * Returns all methods that this router handles. For {@code OPTIONS *}. 339 | */ 340 | public Set allAllowedMethods() { 341 | if (anyMethodRouter.size() > 0) { 342 | Set ret = new HashSet(9); 343 | ret.add(HttpMethod.CONNECT); 344 | ret.add(HttpMethod.DELETE); 345 | ret.add(HttpMethod.GET); 346 | ret.add(HttpMethod.HEAD); 347 | ret.add(HttpMethod.OPTIONS); 348 | ret.add(HttpMethod.PATCH); 349 | ret.add(HttpMethod.POST); 350 | ret.add(HttpMethod.PUT); 351 | ret.add(HttpMethod.TRACE); 352 | return ret; 353 | } else { 354 | return new HashSet(routers.keySet()); 355 | } 356 | } 357 | 358 | //-------------------------------------------------------------------------- 359 | // Reverse routing. 360 | 361 | /** 362 | * Given a target and params, this method tries to do the reverse routing 363 | * and returns the URI. 364 | * 365 | *

Placeholders in the path pattern will be filled with the params. 366 | * The params can be a map of {@code placeholder name -> value} 367 | * or ordered values. 368 | * 369 | *

If a param doesn't have a corresponding placeholder, it will be put 370 | * to the query part of the result URI. 371 | * 372 | * @return {@code null} if there's no match 373 | */ 374 | public String uri(HttpMethod method, T target, Object... params) { 375 | MethodlessRouter router = (method == null) ? anyMethodRouter : routers.get(method); 376 | 377 | // Fallback to anyMethodRouter if no router is found for the method 378 | if (router == null) { 379 | router = anyMethodRouter; 380 | } 381 | 382 | String ret = router.uri(target, params); 383 | if (ret != null) { 384 | return ret; 385 | } 386 | 387 | // Fallback to anyMethodRouter if the router was not anyMethodRouter and no path is found 388 | return (router != anyMethodRouter) ? anyMethodRouter.uri(target, params) : null; 389 | } 390 | 391 | /** 392 | * Given a target and params, this method tries to do the reverse routing 393 | * and returns the URI. 394 | * 395 | *

Placeholders in the path pattern will be filled with the params. 396 | * The params can be a map of {@code placeholder name -> value} 397 | * or ordered values. 398 | * 399 | *

If a param doesn't have a corresponding placeholder, it will be put 400 | * to the query part of the result URI. 401 | * 402 | * @return {@code null} if there's no match 403 | */ 404 | public String uri(T target, Object... params) { 405 | Collection> rs = routers.values(); 406 | for (MethodlessRouter r : rs) { 407 | String ret = r.uri(target, params); 408 | if (ret != null) { 409 | return ret; 410 | } 411 | } 412 | return anyMethodRouter.uri(target, params); 413 | } 414 | 415 | //-------------------------------------------------------------------------- 416 | 417 | /** 418 | * Returns visualized routing rules. 419 | */ 420 | @Override 421 | public String toString() { 422 | // Step 1/2: Dump routers and anyMethodRouter in order 423 | int numRoutes = size(); 424 | List methods = new ArrayList(numRoutes); 425 | List patterns = new ArrayList(numRoutes); 426 | List targets = new ArrayList(numRoutes); 427 | 428 | // For router 429 | for (Entry> e : routers.entrySet()) { 430 | HttpMethod method = e.getKey(); 431 | MethodlessRouter router = e.getValue(); 432 | aggregateRoutes(method.toString(), router.first().routes(), methods, patterns, targets); 433 | aggregateRoutes(method.toString(), router.other().routes(), methods, patterns, targets); 434 | aggregateRoutes(method.toString(), router.last().routes(), methods, patterns, targets); 435 | } 436 | 437 | // For anyMethodRouter 438 | aggregateRoutes("*", anyMethodRouter.first().routes(), methods, patterns, targets); 439 | aggregateRoutes("*", anyMethodRouter.other().routes(), methods, patterns, targets); 440 | aggregateRoutes("*", anyMethodRouter.last().routes(), methods, patterns, targets); 441 | 442 | // For notFound 443 | if (notFound != null) { 444 | methods.add("*"); 445 | patterns.add("*"); 446 | targets.add(targetToString(notFound)); 447 | } 448 | 449 | // Step 2/2: Format the List into aligned columns: 450 | int maxLengthMethod = maxLength(methods); 451 | int maxLengthPattern = maxLength(patterns); 452 | String format = "%-" + maxLengthMethod + "s %-" + maxLengthPattern + "s %s\n"; 453 | int initialCapacity = (maxLengthMethod + 1 + maxLengthPattern + 1 + 20) * methods.size(); 454 | StringBuilder b = new StringBuilder(initialCapacity); 455 | for (int i = 0; i < methods.size(); i++) { 456 | String method = methods.get(i); 457 | String pattern = patterns.get(i); 458 | String target = targets.get(i); 459 | b.append(String.format(format, method, pattern, target)); 460 | } 461 | return b.toString(); 462 | } 463 | 464 | /** 465 | * Helper for toString. 466 | */ 467 | private static void aggregateRoutes( 468 | String method, Map routes, 469 | List accMethods, List accPatterns, List accTargets) { 470 | for (Map.Entry entry : routes.entrySet()) { 471 | accMethods.add(method); 472 | accPatterns.add("/" + entry.getKey().pattern()); 473 | accTargets.add(targetToString(entry.getValue())); 474 | } 475 | } 476 | 477 | /** 478 | * Helper for toString. 479 | */ 480 | private static int maxLength(List coll) { 481 | int max = 0; 482 | for (String e : coll) { 483 | int length = e.length(); 484 | if (length > max) { 485 | max = length; 486 | } 487 | } 488 | return max; 489 | } 490 | 491 | /** 492 | * Helper for toString. 493 | * 494 | *

For example, returns 495 | * "io.netty.example.http.router.HttpRouterServerHandler" instead of 496 | * "class io.netty.example.http.router.HttpRouterServerHandler" 497 | */ 498 | private static String targetToString(Object target) { 499 | if (target instanceof Class) { 500 | return ((Class) target).getName(); 501 | } else { 502 | return target.toString(); 503 | } 504 | } 505 | 506 | //-------------------------------------------------------------------------- 507 | 508 | public Router CONNECT(String path, T target) { 509 | return addRoute(HttpMethod.CONNECT, path, target); 510 | } 511 | 512 | public Router DELETE(String path, T target) { 513 | return addRoute(HttpMethod.DELETE, path, target); 514 | } 515 | 516 | public Router GET(String path, T target) { 517 | return addRoute(HttpMethod.GET, path, target); 518 | } 519 | 520 | public Router HEAD(String path, T target) { 521 | return addRoute(HttpMethod.HEAD, path, target); 522 | } 523 | 524 | public Router OPTIONS(String path, T target) { 525 | return addRoute(HttpMethod.OPTIONS, path, target); 526 | } 527 | 528 | public Router PATCH(String path, T target) { 529 | return addRoute(HttpMethod.PATCH, path, target); 530 | } 531 | 532 | public Router POST(String path, T target) { 533 | return addRoute(HttpMethod.POST, path, target); 534 | } 535 | 536 | public Router PUT(String path, T target) { 537 | return addRoute(HttpMethod.PUT, path, target); 538 | } 539 | 540 | public Router TRACE(String path, T target) { 541 | return addRoute(HttpMethod.TRACE, path, target); 542 | } 543 | 544 | public Router ANY(String path, T target) { 545 | return addRoute(null, path, target); 546 | } 547 | 548 | //-------------------------------------------------------------------------- 549 | 550 | public Router CONNECT_FIRST(String path, T target) { 551 | return addRouteFirst(HttpMethod.CONNECT, path, target); 552 | } 553 | 554 | public Router DELETE_FIRST(String path, T target) { 555 | return addRouteFirst(HttpMethod.DELETE, path, target); 556 | } 557 | 558 | public Router GET_FIRST(String path, T target) { 559 | return addRouteFirst(HttpMethod.GET, path, target); 560 | } 561 | 562 | public Router HEAD_FIRST(String path, T target) { 563 | return addRouteFirst(HttpMethod.HEAD, path, target); 564 | } 565 | 566 | public Router OPTIONS_FIRST(String path, T target) { 567 | return addRouteFirst(HttpMethod.OPTIONS, path, target); 568 | } 569 | 570 | public Router PATCH_FIRST(String path, T target) { 571 | return addRouteFirst(HttpMethod.PATCH, path, target); 572 | } 573 | 574 | public Router POST_FIRST(String path, T target) { 575 | return addRouteFirst(HttpMethod.POST, path, target); 576 | } 577 | 578 | public Router PUT_FIRST(String path, T target) { 579 | return addRouteFirst(HttpMethod.PUT, path, target); 580 | } 581 | 582 | public Router TRACE_FIRST(String path, T target) { 583 | return addRouteFirst(HttpMethod.TRACE, path, target); 584 | } 585 | 586 | public Router ANY_FIRST(String path, T target) { 587 | return addRouteFirst(null, path, target); 588 | } 589 | 590 | //-------------------------------------------------------------------------- 591 | 592 | public Router CONNECT_LAST(String path, T target) { 593 | return addRouteLast(HttpMethod.CONNECT, path, target); 594 | } 595 | 596 | public Router DELETE_LAST(String path, T target) { 597 | return addRouteLast(HttpMethod.DELETE, path, target); 598 | } 599 | 600 | public Router GET_LAST(String path, T target) { 601 | return addRouteLast(HttpMethod.GET, path, target); 602 | } 603 | 604 | public Router HEAD_LAST(String path, T target) { 605 | return addRouteLast(HttpMethod.HEAD, path, target); 606 | } 607 | 608 | public Router OPTIONS_LAST(String path, T target) { 609 | return addRouteLast(HttpMethod.OPTIONS, path, target); 610 | } 611 | 612 | public Router PATCH_LAST(String path, T target) { 613 | return addRouteLast(HttpMethod.PATCH, path, target); 614 | } 615 | 616 | public Router POST_LAST(String path, T target) { 617 | return addRouteLast(HttpMethod.POST, path, target); 618 | } 619 | 620 | public Router PUT_LAST(String path, T target) { 621 | return addRouteLast(HttpMethod.PUT, path, target); 622 | } 623 | 624 | public Router TRACE_LAST(String path, T target) { 625 | return addRouteLast(HttpMethod.TRACE, path, target); 626 | } 627 | 628 | public Router ANY_LAST(String path, T target) { 629 | return addRouteLast(null, path, target); 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /src/main/java/io/netty/handler/codec/http/router/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | 17 | /** 18 | * This package contains HTTP router related classes. 19 | */ 20 | package io.netty.handler.codec.http.router; 21 | -------------------------------------------------------------------------------- /src/test/java/io/netty/example/http/router/HttpRouterServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.example.http.router; 17 | 18 | import io.netty.bootstrap.ServerBootstrap; 19 | import io.netty.channel.Channel; 20 | import io.netty.channel.ChannelOption; 21 | import io.netty.channel.nio.NioEventLoopGroup; 22 | import io.netty.channel.socket.nio.NioServerSocketChannel; 23 | import io.netty.handler.codec.http.router.Router; 24 | 25 | public class HttpRouterServer { 26 | private static final int PORT = 8000; 27 | 28 | public static void main(String[] args) throws Exception { 29 | // This is an example router, it will be used at HttpRouterServerHandler. 30 | // 31 | // For simplicity of this example, route targets are just simple strings. 32 | // But you can make them classes, and at HttpRouterServerHandler once you 33 | // get a target class, you can create an instance of it and dispatch the 34 | // request to the instance etc. 35 | Router router = new Router() 36 | .GET("/", "Index page") 37 | .GET("/articles/:id", "Article show page") 38 | .notFound("404 Not Found"); 39 | System.out.println(router); 40 | 41 | NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); 42 | NioEventLoopGroup workerGroup = new NioEventLoopGroup(); 43 | 44 | try { 45 | ServerBootstrap b = new ServerBootstrap(); 46 | b.group(bossGroup, workerGroup) 47 | .childOption(ChannelOption.TCP_NODELAY, java.lang.Boolean.TRUE) 48 | .childOption(ChannelOption.SO_KEEPALIVE, java.lang.Boolean.TRUE) 49 | .channel(NioServerSocketChannel.class) 50 | .childHandler(new HttpRouterServerInitializer(router)); 51 | 52 | Channel ch = b.bind(PORT).sync().channel(); 53 | System.out.println("Server started: http://127.0.0.1:" + PORT + '/'); 54 | 55 | ch.closeFuture().sync(); 56 | } finally { 57 | bossGroup.shutdownGracefully(); 58 | workerGroup.shutdownGracefully(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/io/netty/example/http/router/HttpRouterServerHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.example.http.router; 17 | 18 | import io.netty.buffer.Unpooled; 19 | import io.netty.channel.ChannelFutureListener; 20 | import io.netty.channel.ChannelHandler; 21 | import io.netty.channel.ChannelHandlerContext; 22 | import io.netty.channel.SimpleChannelInboundHandler; 23 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 24 | import io.netty.handler.codec.http.FullHttpResponse; 25 | import io.netty.handler.codec.http.HttpHeaderNames; 26 | import io.netty.handler.codec.http.HttpHeaderValues; 27 | import io.netty.handler.codec.http.HttpRequest; 28 | import io.netty.handler.codec.http.HttpResponse; 29 | import io.netty.handler.codec.http.HttpResponseStatus; 30 | import io.netty.handler.codec.http.HttpUtil; 31 | import io.netty.handler.codec.http.HttpVersion; 32 | import io.netty.handler.codec.http.router.RouteResult; 33 | import io.netty.handler.codec.http.router.Router; 34 | import io.netty.util.CharsetUtil; 35 | 36 | @ChannelHandler.Sharable 37 | public class HttpRouterServerHandler extends SimpleChannelInboundHandler { 38 | // For simplicity of this example, route targets are just simple strings. 39 | // But you can make them classes, and here once you get a target class, 40 | // you can create an instance of it and dispatch the request to the instance etc. 41 | private final Router router; 42 | 43 | HttpRouterServerHandler(Router router) { 44 | this.router = router; 45 | } 46 | 47 | @Override 48 | public void channelRead0(ChannelHandlerContext ctx, HttpRequest req) { 49 | if (HttpUtil.is100ContinueExpected(req)) { 50 | ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)); 51 | return; 52 | } 53 | 54 | HttpResponse res = createResponse(req, router); 55 | flushResponse(ctx, req, res); 56 | } 57 | 58 | // Display debug info. 59 | private static HttpResponse createResponse(HttpRequest req, Router router) { 60 | RouteResult routeResult = router.route(req.method(), req.uri()); 61 | 62 | String content = 63 | "router: \n" + router + "\n" + 64 | "req: " + req + "\n\n" + 65 | "routeResult: \n" + 66 | "target: " + routeResult.target() + "\n" + 67 | "pathParams: " + routeResult.pathParams() + "\n" + 68 | "queryParams: " + routeResult.queryParams() + "\n\n" + 69 | "allowedMethods: " + router.allowedMethods(req.uri()); 70 | 71 | FullHttpResponse res = new DefaultFullHttpResponse( 72 | HttpVersion.HTTP_1_1, HttpResponseStatus.OK, 73 | Unpooled.copiedBuffer(content, CharsetUtil.UTF_8) 74 | ); 75 | 76 | res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); 77 | res.headers().set(HttpHeaderNames.CONTENT_LENGTH, res.content().readableBytes()); 78 | 79 | return res; 80 | } 81 | 82 | private static void flushResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { 83 | if (!HttpUtil.isKeepAlive(req)) { 84 | ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE); 85 | } else { 86 | res.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 87 | ctx.writeAndFlush(res); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/io/netty/example/http/router/HttpRouterServerInitializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.example.http.router; 17 | 18 | import io.netty.channel.ChannelInitializer; 19 | import io.netty.channel.socket.SocketChannel; 20 | import io.netty.handler.codec.http.HttpServerCodec; 21 | import io.netty.handler.codec.http.BadClientSilencer; 22 | import io.netty.handler.codec.http.router.Router; 23 | 24 | public class HttpRouterServerInitializer extends ChannelInitializer { 25 | private final HttpRouterServerHandler handler; 26 | private final BadClientSilencer badClientSilencer = new BadClientSilencer(); 27 | 28 | HttpRouterServerInitializer(Router router) { 29 | handler = new HttpRouterServerHandler(router); 30 | } 31 | 32 | @Override 33 | public void initChannel(SocketChannel ch) { 34 | ch.pipeline() 35 | .addLast(new HttpServerCodec()) 36 | .addLast(handler) 37 | .addLast(badClientSilencer); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/io/netty/handler/codec/http/router/ReverseRoutingTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http.router; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | import static io.netty.handler.codec.http.HttpMethod.GET; 22 | import static io.netty.handler.codec.http.HttpMethod.POST; 23 | import static io.netty.handler.codec.http.HttpMethod.PUT; 24 | import static org.junit.Assert.assertEquals; 25 | 26 | import org.junit.Before; 27 | import org.junit.Test; 28 | 29 | public class ReverseRoutingTest { 30 | private Router router; 31 | 32 | @Before 33 | public void setUp() { 34 | router = StringRouter.create(); 35 | } 36 | 37 | @Test 38 | public void testHandleMethod() { 39 | assertEquals("/articles", router.uri(GET, "index")); 40 | 41 | assertEquals("/articles/123", router.uri(GET, "show", "id", "123")); 42 | 43 | assertEquals("/anyMethod", router.uri(GET, "anyMethod")); 44 | assertEquals("/anyMethod", router.uri(POST, "anyMethod")); 45 | assertEquals("/anyMethod", router.uri(PUT, "anyMethod")); 46 | } 47 | 48 | @Test 49 | public void testHandleEmptyParams() { 50 | assertEquals("/articles", router.uri("index")); 51 | } 52 | 53 | @Test 54 | public void testHandleMapParams() { 55 | Map map = new HashMap(); 56 | map.put("id", 123); 57 | assertEquals("/articles/123", router.uri("show", map)); 58 | } 59 | 60 | @Test 61 | public void testHandleVarargs() { 62 | assertEquals("/download/foo/bar.png", router.uri("download", "*", "foo/bar.png")); 63 | } 64 | 65 | @Test 66 | public void testReturnPathWithMinimumNumberOfParams() { 67 | Map map1 = new HashMap(); 68 | map1.put("id", 123); 69 | map1.put("format", "json"); 70 | assertEquals("/articles/123/json", router.uri("show", map1)); 71 | 72 | Map map2 = new HashMap(); 73 | map2.put("id", 123); 74 | map2.put("format", "json"); 75 | map2.put("x", 1); 76 | map2.put("y", 2); 77 | String path = router.uri("show", map2); 78 | boolean matched1 = path.equals("/articles/123/json?x=1&y=2"); 79 | boolean matched2 = path.equals("/articles/123/json?y=2&x=1"); 80 | assertEquals(true, matched1 || matched2); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/io/netty/handler/codec/http/router/RoutingTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http.router; 17 | 18 | import static io.netty.handler.codec.http.HttpMethod.GET; 19 | import static io.netty.handler.codec.http.HttpMethod.POST; 20 | import static org.junit.Assert.assertEquals; 21 | import static org.junit.Assert.assertNotNull; 22 | import static org.junit.Assert.assertTrue; 23 | 24 | import io.netty.handler.codec.http.HttpMethod; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | 28 | import java.util.Set; 29 | 30 | public class RoutingTest { 31 | private Router router; 32 | 33 | @Before 34 | public void setUp() { 35 | router = StringRouter.create(); 36 | } 37 | 38 | @Test 39 | public void testIgnoreSlashesAtBothEnds() { 40 | assertEquals("index", router.route(GET, "articles").target()); 41 | assertEquals("index", router.route(GET, "/articles").target()); 42 | assertEquals("index", router.route(GET, "//articles").target()); 43 | assertEquals("index", router.route(GET, "articles/").target()); 44 | assertEquals("index", router.route(GET, "articles//").target()); 45 | assertEquals("index", router.route(GET, "/articles/").target()); 46 | assertEquals("index", router.route(GET, "//articles//").target()); 47 | } 48 | 49 | @Test 50 | public void testHandleEmptyParams() { 51 | RouteResult routed = router.route(GET, "/articles"); 52 | assertEquals("index", routed.target()); 53 | assertEquals(0, routed.pathParams().size()); 54 | } 55 | 56 | @Test 57 | public void testHandleParams() { 58 | RouteResult routed = router.route(GET, "/articles/123"); 59 | assertEquals("show", routed.target()); 60 | assertEquals(1, routed.pathParams().size()); 61 | assertEquals("123", routed.pathParams().get("id")); 62 | } 63 | 64 | @Test 65 | public void testEncodedSlash() { 66 | RouteResult routed = router.route(GET, "/articles/123%2F456"); 67 | assertEquals("show", routed.target()); 68 | assertEquals(1, routed.pathParams().size()); 69 | assertEquals("123/456", routed.pathParams().get("id")); 70 | 71 | assertEquals("/articles/123%2F456", routed.uri()); 72 | assertEquals("/articles/123/456", routed.decodedPath()); 73 | } 74 | 75 | @Test 76 | public void testHandleNone() { 77 | RouteResult routed = router.route(GET, "/noexist"); 78 | assertEquals("404", routed.target()); 79 | } 80 | 81 | @Test 82 | public void testHandleSplatWildcard() { 83 | RouteResult routed = router.route(GET, "/download/foo/bar.png"); 84 | assertEquals("download", routed.target()); 85 | assertEquals(1, routed.pathParams().size()); 86 | assertEquals("foo/bar.png", routed.pathParams().get("*")); 87 | } 88 | 89 | @Test 90 | public void testHandleOrder() { 91 | RouteResult routed1 = router.route(GET, "/articles/new"); 92 | assertEquals("new", routed1.target()); 93 | assertEquals(0, routed1.pathParams().size()); 94 | 95 | RouteResult routed2 = router.route(GET, "/articles/123"); 96 | assertEquals("show", routed2.target()); 97 | assertEquals(1, routed2.pathParams().size()); 98 | assertEquals("123", routed2.pathParams().get("id")); 99 | 100 | RouteResult routed3 = router.route(GET, "/notfound"); 101 | assertEquals("404", routed3.target()); 102 | assertEquals(0, routed3.pathParams().size()); 103 | } 104 | 105 | @Test 106 | public void testHandleAnyMethod() { 107 | RouteResult routed1 = router.route(GET, "/anyMethod"); 108 | assertEquals("anyMethod", routed1.target()); 109 | assertEquals(0, routed1.pathParams().size()); 110 | 111 | RouteResult routed2 = router.route(POST, "/anyMethod"); 112 | assertEquals("anyMethod", routed2.target()); 113 | assertEquals(0, routed2.pathParams().size()); 114 | } 115 | 116 | @Test 117 | public void testHandleRemoveByTarget() { 118 | router.removeTarget("index"); 119 | RouteResult routed = router.route(GET, "/articles"); 120 | assertEquals("404", routed.target()); 121 | } 122 | 123 | @Test 124 | public void testHandleRemoveByPathPattern() { 125 | router.removePathPattern("/articles"); 126 | RouteResult routed = router.route(GET, "/articles"); 127 | assertEquals("404", routed.target()); 128 | } 129 | 130 | @Test 131 | public void testAllowedMethods() { 132 | assertEquals(9, router.allAllowedMethods().size()); 133 | 134 | Set methods = router.allowedMethods("/articles"); 135 | assertEquals(2, methods.size()); 136 | assertTrue(methods.contains(GET)); 137 | assertTrue(methods.contains(POST)); 138 | } 139 | 140 | @Test 141 | public void testHandleSubclasses() { 142 | Router> router = new Router>() 143 | .addRoute(GET, "/articles", Index.class) 144 | .addRoute(GET, "/articles/:id", Show.class); 145 | 146 | RouteResult> routed1 = router.route(GET, "/articles"); 147 | RouteResult> routed2 = router.route(GET, "/articles/123"); 148 | assertNotNull(routed1); 149 | assertNotNull(routed2); 150 | assertEquals(Index.class, routed1.target()); 151 | assertEquals(Show.class, routed2.target()); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/test/java/io/netty/handler/codec/http/router/StringRouter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package io.netty.handler.codec.http.router; 17 | 18 | final class StringRouter { 19 | // Utility classes should not have a public or default constructor. 20 | private StringRouter() { } 21 | 22 | static Router create() { 23 | return new Router() 24 | .GET("/articles", "index") 25 | .GET("/articles/:id", "show") 26 | .GET("/articles/:id/:format", "show") 27 | .GET_FIRST("/articles/new", "new") 28 | .POST("/articles", "post") 29 | .PATCH("/articles/:id", "patch") 30 | .DELETE("/articles/:id", "delete") 31 | .ANY("/anyMethod", "anyMethod") 32 | .GET("/download/:*", "download") 33 | .notFound("404"); 34 | } 35 | } 36 | 37 | interface Action { } 38 | class Index implements Action { } 39 | class Show implements Action { } 40 | --------------------------------------------------------------------------------