org.apache.flink.runtime.rest.handler.router.Router Maven / Gradle / Ivy
/*
* 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.flink.runtime.rest.handler.router;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpMethod;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.QueryStringDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* This is adopted and simplified code from tv.cntt:netty-router library. Compared to original version this one
* defines and guarantees an order of pattern matching routes, drops reverse routing feature and restores
* {@link RouterHandler} which was dropped in tv.cntt:netty-router 2.X.X. Original code:
* https://github.com/sinetja/netty-router/blob/2.2.0/src/main/java/io/netty/handler/codec/http/router/Router.java
*
* Router that contains information about both route matching orders and
* HTTP request methods.
*
*
Routes are guaranteed to be matched in order of their addition.
*
*
Route targets can be any type. In the below example, targets are classes:
*
*
* {@code
* Router router = new Router()
* .GET ("/articles", IndexHandler.class)
* .GET ("/articles/:id", ShowHandler.class)
* .POST ("/articles", CreateHandler.class)
* .GET ("/download/:*", DownloadHandler.class) // ":*" must be the last token
* .GET_FIRST("/articles/new", NewHandler.class); // This will be matched first
* }
*
*
* Slashes at both ends are ignored. These are the same:
*
*
* {@code
* router.GET("articles", IndexHandler.class);
* router.GET("/articles", IndexHandler.class);
* router.GET("/articles/", IndexHandler.class);
* }
*
*
* You can remove routes by target or by path pattern:
*
*
* {@code
* router.removePathPattern("/articles");
* }
*
*
* To match requests use {@link #route(HttpMethod, String)}.
*
*
From the {@link RouteResult} you can extract params embedded in
* the path and from the query part of the request URI.
*
*
{@link #notFound(Object)} will be used as the target when there's no match.
*
*
* {@code
* router.notFound(My404Handler.class);
* }
*
*/
public class Router {
private final Map> routers =
new HashMap>();
private final MethodlessRouter anyMethodRouter =
new MethodlessRouter();
private T notFound;
//--------------------------------------------------------------------------
// Design decision:
// We do not allow access to routers and anyMethodRouter, because we don't
// want to expose MethodlessRouter, OrderlessRouter, and PathPattern.
// Exposing those will complicate the use of this package.
/**
* Helper for toString.
*/
private static void aggregateRoutes(
String method, Map routes,
List accMethods, List accPatterns, List accTargets) {
for (Map.Entry entry : routes.entrySet()) {
accMethods.add(method);
accPatterns.add("/" + entry.getKey().pattern());
accTargets.add(targetToString(entry.getValue()));
}
}
/**
* Helper for toString.
*/
private static int maxLength(List coll) {
int max = 0;
for (String e : coll) {
int length = e.length();
if (length > max) {
max = length;
}
}
return max;
}
/**
* Helper for toString.
*
* For example, returns
* "io.netty.example.http.router.HttpRouterServerHandler" instead of
* "class io.netty.example.http.router.HttpRouterServerHandler"
*/
private static String targetToString(Object target) {
if (target instanceof Class) {
return ((Class) target).getName();
} else {
return target.toString();
}
}
/**
* Returns the fallback target for use when there's no match at
* {@link #route(HttpMethod, String)}.
*/
public T notFound() {
return notFound;
}
/**
* Returns the number of routes in this router.
*/
public int size() {
int ret = anyMethodRouter.size();
for (MethodlessRouter router : routers.values()) {
ret += router.size();
}
return ret;
}
//--------------------------------------------------------------------------
/**
* Add route.
*
* A path pattern can only point to one target. This method does nothing if the pattern
* has already been added.
*/
public Router addRoute(HttpMethod method, String pathPattern, T target) {
getMethodlessRouter(method).addRoute(pathPattern, target);
return this;
}
//--------------------------------------------------------------------------
/**
* Sets the fallback target for use when there's no match at
* {@link #route(HttpMethod, String)}.
*/
public Router notFound(T target) {
this.notFound = target;
return this;
}
private MethodlessRouter getMethodlessRouter(HttpMethod method) {
if (method == null) {
return anyMethodRouter;
}
MethodlessRouter router = routers.get(method);
if (router == null) {
router = new MethodlessRouter();
routers.put(method, router);
}
return router;
}
/**
* Removes the route specified by the path pattern.
*/
public void removePathPattern(String pathPattern) {
for (MethodlessRouter router : routers.values()) {
router.removePathPattern(pathPattern);
}
anyMethodRouter.removePathPattern(pathPattern);
}
/**
* If there's no match, returns the result with {@link #notFound(Object) notFound}
* as the target if it is set, otherwise returns {@code null}.
*/
public RouteResult route(HttpMethod method, String path) {
return route(method, path, Collections.emptyMap());
}
public RouteResult route(HttpMethod method, String path, Map> queryParameters) {
MethodlessRouter router = routers.get(method);
if (router == null) {
router = anyMethodRouter;
}
String[] tokens = decodePathTokens(path);
RouteResult ret = router.route(path, path, queryParameters, tokens);
if (ret != null) {
return new RouteResult(path, path, ret.pathParams(), queryParameters, ret.target());
}
if (router != anyMethodRouter) {
ret = anyMethodRouter.route(path, path, queryParameters, tokens);
if (ret != null) {
return new RouteResult(path, path, ret.pathParams(), queryParameters, ret.target());
}
}
if (notFound != null) {
return new RouteResult(path, path, Collections.emptyMap(), queryParameters, notFound);
}
return null;
}
//--------------------------------------------------------------------------
private String[] decodePathTokens(String uri) {
// Need to split the original URI (instead of QueryStringDecoder#path) then decode the tokens (components),
// otherwise /test1/123%2F456 will not match /test1/:p1
int qPos = uri.indexOf("?");
String encodedPath = (qPos >= 0) ? uri.substring(0, qPos) : uri;
String[] encodedTokens = PathPattern.removeSlashesAtBothEnds(encodedPath).split("/");
String[] decodedTokens = new String[encodedTokens.length];
for (int i = 0; i < encodedTokens.length; i++) {
String encodedToken = encodedTokens[i];
decodedTokens[i] = QueryStringDecoder.decodeComponent(encodedToken);
}
return decodedTokens;
}
/**
* Returns allowed methods for a specific URI.
*
* For {@code OPTIONS *}, use {@link #allAllowedMethods()} instead of this method.
*/
public Set allowedMethods(String uri) {
QueryStringDecoder decoder = new QueryStringDecoder(uri);
String[] tokens = PathPattern.removeSlashesAtBothEnds(decoder.path()).split("/");
if (anyMethodRouter.anyMatched(tokens)) {
return allAllowedMethods();
}
Set ret = new HashSet(routers.size());
for (Map.Entry> entry : routers.entrySet()) {
MethodlessRouter router = entry.getValue();
if (router.anyMatched(tokens)) {
HttpMethod method = entry.getKey();
ret.add(method);
}
}
return ret;
}
/**
* Returns all methods that this router handles. For {@code OPTIONS *}.
*/
public Set allAllowedMethods() {
if (anyMethodRouter.size() > 0) {
Set ret = new HashSet(9);
ret.add(HttpMethod.CONNECT);
ret.add(HttpMethod.DELETE);
ret.add(HttpMethod.GET);
ret.add(HttpMethod.HEAD);
ret.add(HttpMethod.OPTIONS);
ret.add(HttpMethod.PATCH);
ret.add(HttpMethod.POST);
ret.add(HttpMethod.PUT);
ret.add(HttpMethod.TRACE);
return ret;
} else {
return new HashSet(routers.keySet());
}
}
/**
* Returns visualized routing rules.
*/
@Override
public String toString() {
// Step 1/2: Dump routers and anyMethodRouter in order
int numRoutes = size();
List methods = new ArrayList(numRoutes);
List patterns = new ArrayList(numRoutes);
List targets = new ArrayList(numRoutes);
// For router
for (Entry> e : routers.entrySet()) {
HttpMethod method = e.getKey();
MethodlessRouter router = e.getValue();
aggregateRoutes(method.toString(), router.routes(), methods, patterns, targets);
}
// For anyMethodRouter
aggregateRoutes("*", anyMethodRouter.routes(), methods, patterns, targets);
// For notFound
if (notFound != null) {
methods.add("*");
patterns.add("*");
targets.add(targetToString(notFound));
}
// Step 2/2: Format the List into aligned columns:
int maxLengthMethod = maxLength(methods);
int maxLengthPattern = maxLength(patterns);
String format = "%-" + maxLengthMethod + "s %-" + maxLengthPattern + "s %s\n";
int initialCapacity = (maxLengthMethod + 1 + maxLengthPattern + 1 + 20) * methods.size();
StringBuilder b = new StringBuilder(initialCapacity);
for (int i = 0; i < methods.size(); i++) {
String method = methods.get(i);
String pattern = patterns.get(i);
String target = targets.get(i);
b.append(String.format(format, method, pattern, target));
}
return b.toString();
}
//--------------------------------------------------------------------------
public Router addConnect(String path, T target) {
return addRoute(HttpMethod.CONNECT, path, target);
}
public Router addDelete(String path, T target) {
return addRoute(HttpMethod.DELETE, path, target);
}
public Router addGet(String path, T target) {
return addRoute(HttpMethod.GET, path, target);
}
public Router addHead(String path, T target) {
return addRoute(HttpMethod.HEAD, path, target);
}
public Router addOptions(String path, T target) {
return addRoute(HttpMethod.OPTIONS, path, target);
}
public Router addPatch(String path, T target) {
return addRoute(HttpMethod.PATCH, path, target);
}
public Router addPost(String path, T target) {
return addRoute(HttpMethod.POST, path, target);
}
public Router addPut(String path, T target) {
return addRoute(HttpMethod.PUT, path, target);
}
public Router addTrace(String path, T target) {
return addRoute(HttpMethod.TRACE, path, target);
}
public Router addAny(String path, T target) {
return addRoute(null, path, target);
}
}