
io.cdap.http.internal.HttpResourceHandler Maven / Gradle / Ivy
/*
* Copyright © 2017-2019 Cask Data, Inc.
*
* 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 io.cdap.http.internal;
import io.cdap.http.BodyConsumer;
import io.cdap.http.ExceptionHandler;
import io.cdap.http.HandlerContext;
import io.cdap.http.HandlerHook;
import io.cdap.http.HttpHandler;
import io.cdap.http.HttpResponder;
import io.cdap.http.URLRewriter;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nullable;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
/**
* HttpResourceHandler handles the http request. HttpResourceHandler looks up all Jax-rs annotations in classes
* and dispatches to appropriate method on receiving requests.
*/
public final class HttpResourceHandler implements HttpHandler {
private static final Logger LOG = LoggerFactory.getLogger(HttpResourceHandler.class);
// Limit the number of parts of the path so that match score calculation during runtime does not overflow
private static final int MAX_PATH_PARTS = 25;
private final PatternPathRouterWithGroups patternRouter =
PatternPathRouterWithGroups.create(MAX_PATH_PARTS);
private final Iterable handlers;
private final Iterable handlerHooks;
private final URLRewriter urlRewriter;
/**
* Construct HttpResourceHandler. Reads all annotations from all the handler classes and methods passed in, constructs
* patternPathRouter which is routable by path to {@code HttpResourceModel} as destination of the route.
*
* @param handlers Iterable of HttpHandler.
* @param handlerHooks Iterable of HandlerHook.
* @param urlRewriter URL re-writer.
* @param exceptionHandler Exception handler
*/
public HttpResourceHandler(Iterable extends HttpHandler> handlers, Iterable extends HandlerHook> handlerHooks,
URLRewriter urlRewriter, ExceptionHandler exceptionHandler) {
//Store the handlers to call init and destroy on all handlers.
this.handlers = copyOf(handlers);
this.handlerHooks = copyOf(handlerHooks);
this.urlRewriter = urlRewriter;
for (HttpHandler handler : handlers) {
LOG.trace("Parsing handler {}", handler.getClass().getName());
String basePath = "";
if (handler.getClass().isAnnotationPresent(Path.class)) {
basePath = handler.getClass().getAnnotation(Path.class).value();
}
for (Method method : handler.getClass().getDeclaredMethods()) {
Class>[] params = method.getParameterTypes();
if (params.length >= 2
&& (params[0].isAssignableFrom(HttpRequest.class) || params[0].isAssignableFrom(FullHttpRequest.class))
&& params[1].isAssignableFrom(HttpResponder.class)
&& Modifier.isPublic(method.getModifiers())) {
// For streaming consumption, the first param cannot be FullHttpMessage
if (BodyConsumer.class.isAssignableFrom(method.getReturnType())
&& params[0].isAssignableFrom(FullHttpMessage.class)) {
throw new IllegalArgumentException(
"Method with return type as BodyConsumer cannot have FullHttpMessage as the first argument: " + method);
}
String relativePath = "";
if (method.getAnnotation(Path.class) != null) {
relativePath = method.getAnnotation(Path.class).value();
}
String absolutePath = String.format("%s/%s", basePath, relativePath);
Set httpMethods = getHttpMethods(method);
if (httpMethods.isEmpty()) {
throw new IllegalArgumentException("No HttpMethod found for handler method " + method);
}
try {
HttpResourceModel resourceModel = new HttpResourceModel(httpMethods, absolutePath, method,
handler, exceptionHandler);
LOG.trace("Adding resource model {}", resourceModel);
patternRouter.add(absolutePath, resourceModel);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to create http handler from method "
+ method + " in handler class " + handler.getClass().getName(), e);
}
} else {
LOG.trace("Not adding method {}({}) to path routing like. HTTP calls will not be routed to this method",
method.getName(), params);
}
}
}
}
/**
* Fetches the HttpMethod from annotations and returns String representation of HttpMethod.
* Return emptyString if not present.
*
* @param method Method handling the http request.
* @return String representation of HttpMethod from annotations or emptyString as a default.
*/
private Set getHttpMethods(Method method) {
Set httpMethods = new HashSet<>();
if (method.isAnnotationPresent(GET.class)) {
httpMethods.add(HttpMethod.GET);
}
if (method.isAnnotationPresent(PUT.class)) {
httpMethods.add(HttpMethod.PUT);
}
if (method.isAnnotationPresent(POST.class)) {
httpMethods.add(HttpMethod.POST);
}
if (method.isAnnotationPresent(DELETE.class)) {
httpMethods.add(HttpMethod.DELETE);
}
return Collections.unmodifiableSet(httpMethods);
}
/**
* Call the appropriate handler for handling the httprequest. 404 if path is not found. 405 if path is found but
* httpMethod does not match what's configured.
*
* @param request instance of {@code HttpRequest}
* @param responder instance of {@code HttpResponder} to handle the request.
*/
public void handle(HttpRequest request, HttpResponder responder) {
if (urlRewriter != null) {
try {
request.setUri(URI.create(request.uri()).normalize().toString());
if (!urlRewriter.rewrite(request, responder)) {
return;
}
} catch (Throwable t) {
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
String.format("Caught exception processing request. Reason: %s",
t.getMessage()));
LOG.error("Exception thrown during rewriting of uri {}", request.uri(), t);
return;
}
}
try {
String path = URI.create(request.uri()).normalize().getPath();
List> routableDestinations
= patternRouter.getDestinations(path);
PatternPathRouterWithGroups.RoutableDestination matchedDestination =
getMatchedDestination(routableDestinations, request.method(), path);
if (matchedDestination != null) {
//Found a httpresource route to it.
HttpResourceModel httpResourceModel = matchedDestination.getDestination();
// Call preCall method of handler hooks.
boolean terminated = false;
HandlerInfo info = new HandlerInfo(httpResourceModel.getMethod().getDeclaringClass().getName(),
httpResourceModel.getMethod().getName());
for (HandlerHook hook : handlerHooks) {
if (!hook.preCall(request, responder, info)) {
// Terminate further request processing if preCall returns false.
terminated = true;
break;
}
}
// Call httpresource method
if (!terminated) {
// Wrap responder to make post hook calls.
responder = new WrappedHttpResponder(responder, handlerHooks, request, info);
if (httpResourceModel.handle(request, responder, matchedDestination.getGroupNameValues()).isStreaming()) {
responder.sendString(HttpResponseStatus.METHOD_NOT_ALLOWED,
String.format("Body Consumer not supported for internalHttpResponder: %s",
request.uri()));
}
}
} else if (routableDestinations.size() > 0) {
//Found a matching resource but could not find the right HttpMethod so return 405
responder.sendString(HttpResponseStatus.METHOD_NOT_ALLOWED,
String.format("Problem accessing: %s. Reason: Method Not Allowed", request.uri()));
} else {
responder.sendString(HttpResponseStatus.NOT_FOUND, String.format("Problem accessing: %s. Reason: Not Found",
request.uri()));
}
} catch (Throwable t) {
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
String.format("Caught exception processing request. Reason: %s", t.getMessage()));
LOG.error("Exception thrown during request processing for uri {}", request.uri(), t);
}
}
/**
* Call the appropriate handler for handling the httprequest. 404 if path is not found. 405 if path is found but
* httpMethod does not match what's configured.
*
* @param request instance of {@code HttpRequest}
* @param responder instance of {@code HttpResponder} to handle the request.
* @return HttpMethodInfo object, null if urlRewriter rewrite returns false, also when method cannot be invoked.
*/
@Nullable
public HttpMethodInfo getDestinationMethod(HttpRequest request, HttpResponder responder) throws Exception {
if (urlRewriter != null) {
try {
request.setUri(URI.create(request.uri()).normalize().toString());
if (!urlRewriter.rewrite(request, responder)) {
return null;
}
} catch (Throwable t) {
LOG.error("Exception thrown during rewriting of uri {}", request.uri(), t);
throw new HandlerException(HttpResponseStatus.INTERNAL_SERVER_ERROR,
String.format("Caught exception processing request. Reason: %s", t.getMessage()));
}
}
try {
String path = URI.create(request.uri()).normalize().getPath();
List> routableDestinations =
patternRouter.getDestinations(path);
PatternPathRouterWithGroups.RoutableDestination matchedDestination =
getMatchedDestination(routableDestinations, request.method(), path);
if (matchedDestination != null) {
HttpResourceModel httpResourceModel = matchedDestination.getDestination();
// Call preCall method of handler hooks.
boolean terminated = false;
HandlerInfo info = new HandlerInfo(httpResourceModel.getMethod().getDeclaringClass().getName(),
httpResourceModel.getMethod().getName());
for (HandlerHook hook : handlerHooks) {
if (!hook.preCall(request, responder, info)) {
// Terminate further request processing if preCall returns false.
terminated = true;
break;
}
}
// Call httpresource handle method, return the HttpMethodInfo Object.
if (!terminated) {
// Wrap responder to make post hook calls.
responder = new WrappedHttpResponder(responder, handlerHooks, request, info);
return httpResourceModel.handle(request, responder, matchedDestination.getGroupNameValues());
}
} else if (routableDestinations.size() > 0) {
//Found a matching resource but could not find the right HttpMethod so return 405
throw new HandlerException(HttpResponseStatus.METHOD_NOT_ALLOWED, request.uri());
} else {
throw new HandlerException(HttpResponseStatus.NOT_FOUND,
String.format("Problem accessing: %s. Reason: Not Found", request.uri()));
}
} catch (Throwable t) {
if (t instanceof HandlerException) {
throw (HandlerException) t;
}
throw new HandlerException(HttpResponseStatus.INTERNAL_SERVER_ERROR,
String.format("Caught exception processing request. Reason: %s", t.getMessage()), t);
}
return null;
}
/**
* Get HttpResourceModel which matches the HttpMethod of the request.
*
* @param routableDestinations List of ResourceModels.
* @param targetHttpMethod HttpMethod.
* @param requestUri request URI.
* @return RoutableDestination that matches httpMethod that needs to be handled. null if there are no matches.
*/
private PatternPathRouterWithGroups.RoutableDestination
getMatchedDestination(List> routableDestinations,
HttpMethod targetHttpMethod, String requestUri) {
LOG.trace("Routable destinations for request {}: {}", requestUri, routableDestinations);
Iterable requestUriParts = splitAndOmitEmpty(requestUri, '/');
List> matchedDestinations = new ArrayList<>();
long maxScore = 0;
for (PatternPathRouterWithGroups.RoutableDestination destination : routableDestinations) {
HttpResourceModel resourceModel = destination.getDestination();
for (HttpMethod httpMethod : resourceModel.getHttpMethod()) {
if (targetHttpMethod.equals(httpMethod)) {
long score = getWeightedMatchScore(requestUriParts, splitAndOmitEmpty(resourceModel.getPath(), '/'));
LOG.trace("Max score = {}. Weighted score for {} is {}. ", maxScore, destination, score);
if (score > maxScore) {
maxScore = score;
matchedDestinations.clear();
matchedDestinations.add(destination);
} else if (score == maxScore) {
matchedDestinations.add(destination);
}
}
}
}
if (matchedDestinations.size() > 1) {
throw new IllegalStateException(String.format("Multiple matched handlers found for request uri %s: %s",
requestUri, matchedDestinations));
} else if (matchedDestinations.size() == 1) {
return matchedDestinations.get(0);
}
return null;
}
/**
* Generate a weighted score based on position for matches of URI parts.
* The matches are weighted in descending order from left to right.
* Exact match is weighted higher than group match, and group match is weighted higher than wildcard match.
*
* @param requestUriParts the parts of request URI
* @param destUriParts the parts of destination URI
* @return weighted score
*/
private long getWeightedMatchScore(Iterable requestUriParts, Iterable destUriParts) {
// The score calculated below is a base 5 number
// The score will have one digit for one part of the URI
// This will allow for 27 parts in the path since log (Long.MAX_VALUE) to base 5 = 27.13
// We limit the number of parts in the path to 25 using MAX_PATH_PARTS constant above to avoid overflow during
// score calculation
long score = 0;
for (Iterator rit = requestUriParts.iterator(), dit = destUriParts.iterator();
rit.hasNext() && dit.hasNext(); ) {
String requestPart = rit.next();
String destPart = dit.next();
if (requestPart.equals(destPart)) {
score = (score * 5) + 4;
} else if (PatternPathRouterWithGroups.GROUP_PATTERN.matcher(destPart).matches()) {
score = (score * 5) + 3;
} else {
score = (score * 5) + 2;
}
}
return score;
}
@Override
public void init(HandlerContext context) {
for (HttpHandler handler : handlers) {
handler.init(context);
}
}
@Override
public void destroy(HandlerContext context) {
for (HttpHandler handler : handlers) {
try {
handler.destroy(context);
} catch (Throwable t) {
LOG.warn("Exception raised in calling handler.destroy() for handler {}", handler, t);
}
}
}
private static List copyOf(Iterable extends T> iterable) {
List list = new ArrayList<>();
for (T item : iterable) {
list.add(item);
}
return Collections.unmodifiableList(list);
}
/**
* Helper method to split a string by a given character, with empty parts omitted.
*/
private static Iterable splitAndOmitEmpty(final String str, final char splitChar) {
return new Iterable() {
@Override
public Iterator iterator() {
return new Iterator() {
int startIdx = 0;
String next = null;
@Override
public boolean hasNext() {
while (next == null && startIdx < str.length()) {
int idx = str.indexOf(splitChar, startIdx);
if (idx == startIdx) {
// Omit empty string
startIdx++;
continue;
}
if (idx >= 0) {
// Found the next part
next = str.substring(startIdx, idx);
startIdx = idx;
} else {
// The last part
if (startIdx < str.length()) {
next = str.substring(startIdx);
startIdx = str.length();
}
break;
}
}
return next != null;
}
@Override
public String next() {
if (hasNext()) {
String next = this.next;
this.next = null;
return next;
}
throw new NoSuchElementException("No more element");
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove not supported");
}
};
}
};
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy