com.blade.mvc.route.RouteMatcher Maven / Gradle / Ivy
package com.blade.mvc.route;
import com.blade.exception.BladeException;
import com.blade.ioc.annotation.Order;
import com.blade.kit.Assert;
import com.blade.kit.BladeKit;
import com.blade.kit.PathKit;
import com.blade.kit.ReflectKit;
import com.blade.mvc.hook.Signature;
import com.blade.mvc.hook.WebHook;
import com.blade.mvc.http.HttpMethod;
import com.blade.mvc.http.Request;
import com.blade.mvc.http.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Default Route Matcher
*
* @author biezhi
* @since 1.7.1-release
*/
public class RouteMatcher {
private static final Logger log = LoggerFactory.getLogger(RouteMatcher.class);
private static final Pattern PATH_VARIABLE_PATTERN = Pattern.compile(":(\\w+)");
private static final String PATH_VARIABLE_REPLACE = "([^/]+)";
private static final String METHOD_NAME = "handle";
// Storage URL and route
private Map routes = new HashMap<>();
private Map> hooks = new HashMap<>();
private List middleware = null;
private Map classMethodPool = new ConcurrentHashMap<>();
private Map, Object> controllerPool = new ConcurrentHashMap<>();
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 Route addRoute(HttpMethod httpMethod, String path, RouteHandler handler, String methodName) throws NoSuchMethodException {
Class> handleType = handler.getClass();
Method method = handleType.getMethod(methodName, Request.class, Response.class);
return addRoute(httpMethod, path, handler, RouteHandler.class, method);
}
public Route addRoute(HttpMethod httpMethod, String path, Object controller, Class> controllerType, Method method) {
// [/** | /*]
path = "*".equals(path) ? "/.*" : path;
path = path.replace("/**", "/.*").replace("/*", "/.*");
String key = path + "#" + httpMethod.toString();
// exist
if (this.routes.containsKey(key)) {
log.warn("\tRoute {} -> {} has exist", path, httpMethod.toString());
}
Route route = new Route(httpMethod, path, controller, controllerType, method);
if (BladeKit.isWebHook(httpMethod)) {
Order order = controllerType.getAnnotation(Order.class);
if (null != order) {
route.setSort(order.value());
}
if (this.hooks.containsKey(key)) {
this.hooks.get(key).add(route);
} else {
List empty = new ArrayList<>();
empty.add(route);
this.hooks.put(key, empty);
}
} else {
this.routes.put(key, route);
}
return route;
}
public void addRoute(String path, RouteHandler handler, HttpMethod httpMethod) {
try {
addRoute(httpMethod, path, handler, METHOD_NAME);
} catch (Exception e) {
log.error("", e);
}
}
public void route(String path, Class> clazz, String methodName) {
Assert.notNull(methodName, "Method name not is null");
HttpMethod httpMethod = HttpMethod.ALL;
if (methodName.contains(":")) {
String[] methodArr = methodName.split(":");
httpMethod = HttpMethod.valueOf(methodArr[0].toUpperCase());
methodName = methodArr[1];
}
this.route(path, clazz, methodName, httpMethod);
}
public void route(String path, Class> clazz, String methodName, HttpMethod httpMethod) {
try {
Assert.notNull(path, "Route path not is null!");
Assert.notNull(clazz, "Class EventType not is null!");
Assert.notNull(methodName, "Method name not is null");
Assert.notNull(httpMethod, "Request Method not is null");
Method[] methods = classMethodPool.computeIfAbsent(clazz.getName(), k -> clazz.getMethods());
if (null != methods) {
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Object controller = controllerPool.computeIfAbsent(clazz, k -> ReflectKit.newInstance(clazz));
addRoute(httpMethod, path, controller, clazz, method);
}
}
}
} catch (Exception e) {
log.error("", e);
}
}
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 uriVariables = new LinkedHashMap<>();
HttpMethod requestMethod = HttpMethod.valueOf(httpMethod);
try {
Pattern pattern = regexRoutePatterns.get(requestMethod);
if (null == pattern) {
return null;
}
Matcher matcher = null;
if (path != null) {
matcher = pattern.matcher(path);
}
boolean matched = false;
if (matcher != null) {
matched = matcher.matches();
}
if (!matched) {
requestMethod = HttpMethod.ALL;
pattern = regexRoutePatterns.get(requestMethod);
if (null == pattern) {
return null;
}
if (path != null) {
matcher = pattern.matcher(path);
}
matched = matcher != null && 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);
log.trace("lookup path: " + path + " uri variables: " + uriVariables);
}
return route;
} catch (Exception e) {
throw new BladeException(e);
}
}
/**
* Find all in before of the hook
*
* @param path request path
*/
public List getBefore(String path) {
String cleanPath = parsePath(path);
List collect = hooks.values().stream()
.flatMap(Collection::stream)
.sorted(Comparator.comparingInt(Route::getSort))
.filter(route -> route.getHttpMethod() == HttpMethod.BEFORE && matchesPath(route.getPath(), cleanPath))
.collect(Collectors.toList());
this.giveMatch(path, collect);
return collect;
}
/**
* Find all in after of the hooks
*
* @param path request path
*/
public List getAfter(String path) {
String cleanPath = parsePath(path);
List afters = hooks.values().stream()
.flatMap(Collection::stream)
.sorted(Comparator.comparingInt(Route::getSort))
.filter(route -> route.getHttpMethod() == HttpMethod.AFTER && matchesPath(route.getPath(), cleanPath))
.collect(Collectors.toList());
this.giveMatch(path, afters);
return afters;
}
public List getMiddleware() {
return this.middleware;
}
/**
* Sort of path
*
* @param uri request uri
* @param routes route list
*/
private void giveMatch(final String uri, List routes) {
routes.stream().sorted((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(PathKit.VAR_REGEXP, PathKit.VAR_REPLACE);
return pathToMatch.matches("(?i)" + routePath);
}
/**
* Parse PathKit
*
* @param path route path
* @return return parsed path
*/
private String parsePath(String path) {
path = PathKit.fixPath(path);
try {
URI uri = new URI(path);
return uri.getPath();
} catch (URISyntaxException e) {
log.error("parse [" + path + "] error", e);
return null;
}
}
// a bad way
public void register() {
routes.values().forEach(route -> log.info("Add route => {}", route));
hooks.values().forEach(route -> log.info("Add hook => {}", route));
List routeHandlers = new ArrayList<>(routes.values());
routeHandlers.addAll(hooks.values().stream().findAny().orElse(new ArrayList<>()));
Stream.of(routes.values(), hooks.values().stream().findAny().orElse(new ArrayList<>()))
.flatMap(Collection::stream).forEach(this::registerRoute);
patternBuilders.keySet().stream()
.filter(BladeKit::notIsWebHook)
.forEach(httpMethod -> {
StringBuilder patternBuilder = patternBuilders.get(httpMethod);
if (patternBuilder.length() > 1) {
patternBuilder.setCharAt(patternBuilder.length() - 1, '$');
}
log.debug("Fast Route Method: {}, regex: {}", httpMethod, patternBuilder);
regexRoutePatterns.put(httpMethod, Pattern.compile(patternBuilder.toString()));
});
}
private void registerRoute(Route route) {
String path = parsePath(route.getPath());
Matcher matcher = null;
if (path != null) {
matcher = PATH_VARIABLE_PATTERN.matcher(path);
}
boolean find = false;
List uriVariableNames = new ArrayList<>();
while (matcher != null && matcher.find()) {
if (!find) {
find = true;
}
String group = matcher.group(0);
uriVariableNames.add(group.substring(1)); // {id} -> id
}
HttpMethod httpMethod = route.getHttpMethod();
if (find || BladeKit.isWebHook(httpMethod)) {
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);
if (matcher != null) {
patternBuilders.get(httpMethod).append("(").append(matcher.replaceAll(PATH_VARIABLE_REPLACE)).append(")|");
}
} else {
String routeKey = path + '#' + httpMethod.toString();
staticRoutes.putIfAbsent(routeKey, route);
}
}
public void clear() {
this.routes.clear();
this.hooks.clear();
this.classMethodPool.clear();
this.controllerPool.clear();
this.regexRoutePatterns.clear();
this.staticRoutes.clear();
this.regexRoutes.clear();
this.indexes.clear();
this.patternBuilders.clear();
}
public void initMiddleware(List hooks) {
this.middleware = hooks.stream().map(webHook -> {
Method method = ReflectKit.getMethod(WebHook.class, "before", Signature.class);
return new Route(HttpMethod.BEFORE, "/.*", webHook, WebHook.class, method);
}).collect(Collectors.toList());
}
private class FastRouteMappingInfo {
Route route;
List variableNames;
FastRouteMappingInfo(Route route, List variableNames) {
this.route = route;
this.variableNames = variableNames;
}
public Route getRoute() {
return route;
}
List getVariableNames() {
return variableNames;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy