/** * Copyright (c) 2015, biezhi 王爵 (biezhi.me@gmail.com) * <p> * Licensed 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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 com.blade.mvc.route; import com.blade.Blade; import com.blade.exception.BladeException; import com.blade.kit.CollectionKit; import com.blade.mvc.http.HttpMethod; import com.blade.mvc.http.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.net.URISyntaxException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Default Route Matcher * * @author <a href="mailto:biezhi.me@gmail.com" target="_blank">biezhi</a> * @since 1.7.1-release */ public class RouteMatcher { private static final Logger LOGGER = LoggerFactory.getLogger(RouteMatcher.class); // Storage URL and route private Map<String, Route> routes = null; private Map<String, Route> interceptors = null; private List<Route> interceptorRoutes = CollectionKit.newArrayList(8); private static final Pattern PATH_VARIABLE_PATTERN = Pattern.compile(":(\\w+)"); private static final String PATH_VARIABLE_REPLACE = "([^/]+)"; private Map<HttpMethod, Map<Integer, FastRouteMappingInfo>> regexRoutes = new HashMap<>(); private Map<String, Route> staticRoutes = new HashMap<>(); private Map<HttpMethod, Pattern> regexRoutePatterns = new HashMap<>(); private Map<HttpMethod, Integer> indexes = new HashMap<>(); private Map<HttpMethod, StringBuilder> patternBuilders = new HashMap<>(); public RouteMatcher(Routers routers) { this.update(routers); } public void update() { this.update(Blade.$().routers()); } public void update(Routers routers) { this.routes = routers.getRoutes(); this.interceptors = routers.getInterceptors(); Collection<Route> inters = interceptors.values(); if (!inters.isEmpty()) { this.interceptorRoutes.addAll(inters); } this.staticRoutes.clear(); this.regexRoutePatterns.clear(); this.regexRoutes.clear(); this.indexes.clear(); this.register(); } public Route lookupRoute(String httpMethod, String path) throws BladeException { path = parsePath(path); String routeKey = path + '#' + httpMethod.toUpperCase(); Route route = staticRoutes.get(routeKey); if (null != route) { return route; } route = staticRoutes.get(path + "#ALL"); if (null != route) { return route; } Map<String, String> uriVariables = CollectionKit.newLinkedHashMap(); HttpMethod requestMethod = HttpMethod.valueOf(httpMethod); try { Matcher matcher = regexRoutePatterns.get(requestMethod).matcher(path); boolean matched = matcher.matches(); if (!matched) { requestMethod = HttpMethod.ALL; matcher = regexRoutePatterns.get(requestMethod).matcher(path); matched = matcher.matches(); } if (matched) { int i; for (i = 1; matcher.group(i) == null; i++) ; FastRouteMappingInfo mappingInfo = regexRoutes.get(requestMethod).get(i); route = mappingInfo.getRoute(); // find path variable String uriVariable; int j = 0; while (++i <= matcher.groupCount() && (uriVariable = matcher.group(i)) != null) { uriVariables.put(mappingInfo.getVariableNames().get(j++), uriVariable); } route.setPathParams(uriVariables); LOGGER.trace("lookup path: " + path + " uri variables: " + uriVariables); } return route; } catch (Exception e) { throw new BladeException(e); } } /** * Find a route * * @param httpMethod httpMethod * @param path request path * @return return route object */ public Route getRoute(String httpMethod, String path) { try { return lookupRoute(httpMethod, path); } catch (BladeException e) { Throwable t = e.getCause(); if (t instanceof NullPointerException) { } else { LOGGER.warn("", e); } } return null; } /** * Find all in before of the interceptor * * @param path request path * @return return interceptor list */ public List<Route> getBefore(String path) { List<Route> befores = CollectionKit.newArrayList(); String cleanPath = parsePath(path); interceptorRoutes.forEach(route -> { if (matchesPath(route.getPath(), cleanPath) && route.getHttpMethod() == HttpMethod.BEFORE) { befores.add(route); } }); this.giveMatch(path, befores); return befores; } /** * Find all in after of the interceptor * * @param path request path * @return return interceptor list */ public List<Route> getAfter(String path) { List<Route> afters = CollectionKit.newArrayList(); String cleanPath = parsePath(path); interceptorRoutes.forEach(route -> { if (matchesPath(route.getPath(), cleanPath) && route.getHttpMethod() == HttpMethod.AFTER) { afters.add(route); } }); this.giveMatch(path, afters); return afters; } /** * Sort of path * * @param uri request uri * @param routes route list */ private void giveMatch(final String uri, List<Route> routes) { Collections.sort(routes, (o1, o2) -> { if (o2.getPath().equals(uri)) { return o2.getPath().indexOf(uri); } return -1; }); } /** * Matching path * * @param routePath route path * @param pathToMatch match path * @return return match is success */ private boolean matchesPath(String routePath, String pathToMatch) { routePath = routePath.replaceAll(Path.VAR_REGEXP, Path.VAR_REPLACE); return pathToMatch.matches("(?i)" + routePath); } /** * Parse Path * * @param path route path * @return return parsed path */ private String parsePath(String path) { path = Path.fixPath(path); try { URI uri = new URI(path); return uri.getPath(); } catch (URISyntaxException e) { throw new BladeException(e); } } // a bad way void register() { List<Route> routeHandlers = new ArrayList<>(routes.values()); routeHandlers.addAll(interceptors.values()); for (Route route : routeHandlers) { String path = parsePath(route.getPath()); Matcher matcher = PATH_VARIABLE_PATTERN.matcher(path); boolean find = false; List<String> uriVariableNames = new ArrayList<>(); while (matcher.find()) { if (!find) { find = true; } String group = matcher.group(0); uriVariableNames.add(group.substring(1)); // {id} -> id } HttpMethod httpMethod = route.getHttpMethod(); if (find || (httpMethod == HttpMethod.AFTER || httpMethod == HttpMethod.BEFORE)) { if (regexRoutes.get(httpMethod) == null) { regexRoutes.put(httpMethod, new HashMap<>()); patternBuilders.put(httpMethod, new StringBuilder("^")); indexes.put(httpMethod, 1); } int i = indexes.get(httpMethod); regexRoutes.get(httpMethod).put(i, new FastRouteMappingInfo(route, uriVariableNames)); indexes.put(httpMethod, i + uriVariableNames.size() + 1); patternBuilders.get(httpMethod).append("(").append(matcher.replaceAll(PATH_VARIABLE_REPLACE)).append(")|"); } else { String routeKey = path + '#' + httpMethod.toString(); if (staticRoutes.get(routeKey) == null) { staticRoutes.put(routeKey, route); } } } for (Map.Entry<HttpMethod, StringBuilder> entry : patternBuilders.entrySet()) { HttpMethod httpMethod = entry.getKey(); StringBuilder patternBuilder = entry.getValue(); if (patternBuilder.length() > 1) { patternBuilder.setCharAt(patternBuilder.length() - 1, '$'); } LOGGER.debug("Fast Route Method: {}, regex: {}", httpMethod, patternBuilder); regexRoutePatterns.put(httpMethod, Pattern.compile(patternBuilder.toString())); } } private class FastRouteMappingInfo { private Route route; private List<String> variableNames; public FastRouteMappingInfo(Route route, List<String> variableNames) { this.route = route; this.variableNames = variableNames; } public Route getRoute() { return route; } public List<String> getVariableNames() { return variableNames; } } }