package org.rakam.server.http;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.util.AttributeKey;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static io.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_HEADERS;
import static io.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_METHODS;
import static io.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_EXPOSE_HEADERS;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
public class RouteMatcher
{
private HashMap<PatternBinding, HttpRequestHandler> routes = new HashMap();
private HttpRequestHandler noMatch = request -> request.response("404", HttpResponseStatus.NOT_FOUND).end();
static final AttributeKey<String> PATH = AttributeKey.valueOf("/path");
private List<Map.Entry<PatternBinding, HttpRequestHandler>> prefixRoutes = new LinkedList<>();
public void handle(ChannelHandlerContext ctx, WebSocketFrame frame)
{
String path = ctx.attr(PATH).get();
final Object handler = routes.get(new PatternBinding(HttpMethod.GET, path));
if (handler != null) {
if (handler instanceof WebSocketService) {
((WebSocketService) handler).handle(ctx, frame);
}
}
else {
// TODO: WHAT TO DO?
ctx.close();
}
}
public void handle(RakamHttpRequest request)
{
String path = cleanPath(request.path());
int lastIndex = path.length() - 1;
if (lastIndex > 0 && path.charAt(lastIndex) == '/') {
path = path.substring(0, lastIndex);
}
HttpMethod method = request.getMethod();
final HttpRequestHandler handler = routes.get(new PatternBinding(method, path));
if (handler != null) {
if (handler instanceof WebSocketService) {
request.context().attr(PATH).set(path);
}
handler.handle(request);
}
else {
for (Map.Entry<PatternBinding, HttpRequestHandler> prefixRoute : prefixRoutes) {
if (method.equals(prefixRoute.getKey().method) && path.startsWith(prefixRoute.getKey().pattern)) {
prefixRoute.getValue().handle(request);
return;
}
}
noMatch.handle(request);
}
}
private String cleanPath(String path)
{
StringBuilder builder = new StringBuilder();
boolean edge = false;
int length = path.length();
for (int i = 0; i < length; i++) {
char c = path.charAt(i);
if (c == '/') {
if (!edge) {
builder.append(c);
}
edge = true;
}
else {
builder.append(c);
edge = false;
}
}
return builder.toString();
}
public void add(String path, WebSocketService handler)
{
PatternBinding key = new PatternBinding(HttpMethod.GET, path);
routes.put(key, handler);
}
public void add(HttpMethod method, String path, HttpRequestHandler handler)
{
if (path.endsWith("*")) {
String substring = path.substring(0, path.length() - 1);
routes.put(new PatternBinding(method, substring), handler);
prefixRoutes.add(new AbstractMap.SimpleImmutableEntry<>(new PatternBinding(method, substring), handler));
}
else {
if (path.length() > 1 && path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
routes.put(new PatternBinding(method, path), handler);
}
}
public void noMatch(HttpRequestHandler handler)
{
noMatch = handler;
}
public static class PatternBinding
{
final HttpMethod method;
final String pattern;
private PatternBinding(HttpMethod method, String pattern)
{
this.method = method;
this.pattern = pattern;
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (!(o instanceof PatternBinding)) {
return false;
}
PatternBinding that = (PatternBinding) o;
if (!method.equals(that.method)) {
return false;
}
if (!pattern.equals(that.pattern)) {
return false;
}
return true;
}
@Override
public int hashCode()
{
int result = method.hashCode();
result = 31 * result + pattern.hashCode();
return result;
}
}
public static class MicroRouteMatcher
{
private final RouteMatcher routeMatcher;
private String path;
public MicroRouteMatcher(RouteMatcher routeMatcher, String path)
{
this.routeMatcher = routeMatcher;
this.path = path;
}
public void add(String lastPath, HttpMethod method, HttpRequestHandler handler)
{
Objects.requireNonNull(path, "path is not configured");
routeMatcher.add(method, path.equals("/") ? lastPath : path + lastPath, handler);
}
}
}