
com.blade.mvc.route.RouteMatcher Maven / Gradle / Ivy
/**
* Copyright (c) 2015, biezhi 王爵 ([email protected])
*
* 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
*
* 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 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 biezhi
* @since 1.7.1-release
*/
public class RouteMatcher {
private static final Logger LOGGER = LoggerFactory.getLogger(RouteMatcher.class);
// Storage URL and route
private Map routes = null;
private Map interceptors = null;
private List interceptorRoutes = CollectionKit.newArrayList(8);
private static final Pattern PATH_VARIABLE_PATTERN = Pattern.compile(":(\\w+)");
private static final String PATH_VARIABLE_REPLACE = "([^/]+)";
private Map> regexRoutes = new HashMap<>();
private Map staticRoutes = new HashMap<>();
private Map regexRoutePatterns = new HashMap<>();
private Map indexes = new HashMap<>();
private Map 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 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){
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 uriVariables = CollectionKit.newLinkedHashMap();
HttpMethod requestMethod = HttpMethod.valueOf(httpMethod);
Matcher matcher = regexRoutePatterns.get(requestMethod).matcher(path);
boolean matched = matcher.matches();
if(!matched){
matcher = regexRoutePatterns.get(HttpMethod.ALL).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;
}
/**
* Find a route
* @param httpMethod httpMethod
* @param path request path
* @return return route object
*/
public Route getRoute(String httpMethod, String path) {
return lookupRoute(httpMethod, path);
}
/**
* Find all in before of the interceptor
* @param path request path
* @return return interceptor list
*/
public List getBefore(String path) {
List 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 getAfter(String path) {
List 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 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 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 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 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 variableNames;
public FastRouteMappingInfo(Route route, List variableNames) {
this.route = route;
this.variableNames = variableNames;
}
public Route getRoute() {
return route;
}
public List getVariableNames() {
return variableNames;
}
}
}