/* * Copyright 2015 The Netty Project * * The Netty Project 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 io.netty.handler.codec.http.router; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Router that doesn't contain information about HTTP request methods and route * matching orders. */ final class OrderlessRouter<T> { private static final InternalLogger log = InternalLoggerFactory.getInstance(OrderlessRouter.class); // A path can only point to one target private final Map<Path, T> routes = new HashMap<Path, T>(); // Reverse index to create reverse routes fast (a target can have multiple paths) private final Map<T, Set<Path>> reverseRoutes = new HashMap<T, Set<Path>>(); //-------------------------------------------------------------------------- /** Returns all routes in this router, an unmodifiable map of {@code Path -> Target}. */ public Map<Path, T> routes() { return Collections.unmodifiableMap(routes); } /** * This method does nothing if the path has already been added. * A path can only point to one target. */ public OrderlessRouter<T> addRoute(String path, T target) { Path p = new Path(path); if (routes.containsKey(path)) { return this; } routes.put(p, target); addReverseRoute(target, p); return this; } private void addReverseRoute(T target, Path path) { Set<Path> paths = reverseRoutes.get(target); if (paths == null) { paths = new HashSet<Path>(); paths.add(path); reverseRoutes.put(target, paths); } else { paths.add(path); } } //-------------------------------------------------------------------------- /** Removes the route specified by the path. */ public void removePath(String path) { Path p = new Path(path); T target = routes.remove(p); if (target == null) { return; } Set<Path> paths = reverseRoutes.remove(target); paths.remove(p); } /** Removes all routes leading to the target. */ public void removeTarget(T target) { Set<Path> paths = reverseRoutes.remove(ObjectUtil.checkNotNull(target, "target")); if (paths == null) { return; } // A path can only point to one target. // A target can have multiple paths. // Remove all paths leading to this target. for (Path path : paths) { routes.remove(path); } } //-------------------------------------------------------------------------- /** @return {@code null} if no match; note: {@code queryParams} is not set in {@link RouteResult} */ public RouteResult<T> route(String path) { return route(Path.removeSlashesAtBothEnds(path).split("/")); } /** @return {@code null} if no match; note: {@code queryParams} is not set in {@link RouteResult} */ public RouteResult<T> route(String[] requestPathTokens) { // Optimization note: // - Reuse tokens and pathParams in the loop // - decoder doesn't decode anything if decoder.parameters is not called Map<String, String> pathParams = new HashMap<String, String>(); for (Map.Entry<Path, T> entry : routes.entrySet()) { Path path = entry.getKey(); if (path.match(requestPathTokens, pathParams)) { T target = entry.getValue(); return new RouteResult(target, pathParams, Collections.emptyMap()); } // Reset for the next loop pathParams.clear(); } return null; } /** Checks if there's any matching route. */ public boolean anyMatched(String[] requestPathTokens) { Map<String, String> pathParams = new HashMap<String, String>(); for (Path path : routes.keySet()) { if (path.match(requestPathTokens, pathParams)) { return true; } // Reset for the next loop pathParams.clear(); } return false; } //-------------------------------------------------------------------------- /** * Given a target and params, this method tries to do the reverse routing * and returns the path. * * The params are put to placeholders in the path. * The params can be a map of {@code placeholder name -> value} * or ordered values. If a param doesn't have a placeholder, it will be put * to the query part of the path. * * @return {@code null} if there's no match, or the params can't be UTF-8 encoded */ @SuppressWarnings("unchecked") public String path(T target, Object... params) { if (params.length == 0) { return path(target, Collections.emptyMap()); } if (params.length == 1 && params[0] instanceof Map<?, ?>) { return pathMap(target, (Map<Object, Object>) params[0]); } if (params.length % 2 == 1) { throw new IllegalArgumentException("Missing value for param: " + params[params.length - 1]); } Map<Object, Object> map = new HashMap<Object, Object>(params.length / 2); for (int i = 0; i < params.length; i += 2) { String key = params[i].toString(); String value = params[i + 1].toString(); map.put(key, value); } return pathMap(target, map); } /** @return {@code null} if there's no match, or the params can't be UTF-8 encoded */ private String pathMap(T target, Map<Object, Object> params) { Set<Path> paths = reverseRoutes.get(target); if (paths == null) { return null; } try { // The best one is the one with minimum number of params in the query String bestCandidate = null; int minQueryParams = Integer.MAX_VALUE; boolean matched = true; Set<String> usedKeys = new HashSet<String>(); for (Path path : paths) { matched = true; usedKeys.clear(); // "+ 16": Just in case the part befor that is 0 int initialCapacity = path.path().length() + 20 * params.size() + 16; StringBuilder b = new StringBuilder(initialCapacity); for (String token : path.tokens()) { b.append('/'); if (token.length() > 0 && token.charAt(0) == ':') { String key = token.substring(1); Object value = params.get(key); if (value == null) { matched = false; break; } usedKeys.add(key); b.append(value.toString()); } else { b.append(token); } } if (matched) { int numQueryParams = params.size() - usedKeys.size(); if (numQueryParams < minQueryParams) { if (numQueryParams > 0) { boolean firstQueryParam = true; for (Map.Entry<Object, Object> entry : params.entrySet()) { String key = entry.getKey().toString(); if (!usedKeys.contains(key)) { if (firstQueryParam) { b.append('?'); firstQueryParam = false; } else { b.append('&'); } String value = entry.getValue().toString(); // May throw UnsupportedEncodingException b.append(URLEncoder.encode(key, "UTF-8")); b.append('='); // May throw UnsupportedEncodingException b.append(URLEncoder.encode(value, "UTF-8")); } } } bestCandidate = b.toString(); minQueryParams = numQueryParams; } } } return bestCandidate; } catch (UnsupportedEncodingException e) { log.warn("Params can't be UTF-8 encoded: " + params); return null; } } }