All Downloads are FREE. Search and download functionalities are using the official Maven repository.

co.cask.http.HttpResourceHandler Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
/*
 * Copyright © 2014 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 co.cask.http;

import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.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.Iterator;
import java.util.List;
import java.util.Set;
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);

  private final PatternPathRouterWithGroups patternRouter = PatternPathRouterWithGroups.create();
  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 handlers, Iterable handlerHooks,
                             URLRewriter urlRewriter, ExceptionHandler exceptionHandler) {
    //Store the handlers to call init and destroy on all handlers.
    this.handlers = ImmutableList.copyOf(handlers);
    this.handlerHooks = ImmutableList.copyOf(handlerHooks);
    this.urlRewriter = urlRewriter;

    for (HttpHandler handler : handlers) {
      String basePath = "";
      if (handler.getClass().isAnnotationPresent(Path.class)) {
        basePath =  handler.getClass().getAnnotation(Path.class).value();
      }

      for (Method method:  handler.getClass().getDeclaredMethods()) {
        if (method.getParameterTypes().length >= 2 &&
          method.getParameterTypes()[0].isAssignableFrom(HttpRequest.class) &&
          method.getParameterTypes()[1].isAssignableFrom(HttpResponder.class) &&
          Modifier.isPublic(method.getModifiers())) {

          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);
          Preconditions.checkArgument(httpMethods.size() >= 1,
                                      String.format("No HttpMethod found for method: %s", method.getName()));
          patternRouter.add(absolutePath, new HttpResourceModel(httpMethods, absolutePath, method,
                                                                handler, exceptionHandler));
        } else {
          LOG.trace("Not adding method {}({}) to path routing like. HTTP calls will not be routed to this method",
                    method.getName(), method.getParameterTypes());
        }
      }
    }
  }

  /**
   * 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 = Sets.newHashSet();

    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 ImmutableSet.copyOf(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.getUri()).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.getUri(), t);
        return;
      }
    }

    try {
      String path = URI.create(request.getUri()).normalize().getPath();

      List> routableDestinations
        = patternRouter.getDestinations(path);

      PatternPathRouterWithGroups.RoutableDestination matchedDestination =
        getMatchedDestination(routableDestinations, request.getMethod(), 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.getUri()));
          }
        }
      } 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.getUri()));
      } else {
        responder.sendString(HttpResponseStatus.NOT_FOUND, String.format("Problem accessing: %s. Reason: Not Found",
                                                                         request.getUri()));
      }
    } 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.getUri(), 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.
   */
  public HttpMethodInfo getDestinationMethod(HttpRequest request, HttpResponder responder) throws Exception {
    if (urlRewriter != null) {
      try {
        request.setUri(URI.create(request.getUri()).normalize().toString());
        if (!urlRewriter.rewrite(request, responder)) {
          return null;
        }
      } catch (Throwable t) {
        LOG.error("Exception thrown during rewriting of uri {}", request.getUri(), t);
        throw new HandlerException(HttpResponseStatus.INTERNAL_SERVER_ERROR,
                                   String.format("Caught exception processing request. Reason: %s", t.getMessage()));
      }
    }

    try {
      String path = URI.create(request.getUri()).normalize().getPath();

      List> routableDestinations =
        patternRouter.getDestinations(path);

      PatternPathRouterWithGroups.RoutableDestination matchedDestination =
        getMatchedDestination(routableDestinations, request.getMethod(), 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.getUri());
      } else {
        throw new HandlerException(HttpResponseStatus.NOT_FOUND,
                                   String.format("Problem accessing: %s. Reason: Not Found", request.getUri()));
      }
    } 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) {

    Iterable requestUriParts = Splitter.on('/').omitEmptyStrings().split(requestUri);
    List> matchedDestinations =
      Lists.newArrayListWithExpectedSize(routableDestinations.size());
    int maxExactMatch = 0;
    int maxGroupMatch = 0;
    int maxPatternLength = 0;

    for (PatternPathRouterWithGroups.RoutableDestination destination : routableDestinations) {
      HttpResourceModel resourceModel = destination.getDestination();
      int groupMatch = destination.getGroupNameValues().size();

      for (HttpMethod httpMethod : resourceModel.getHttpMethod()) {
        if (targetHttpMethod.equals(httpMethod)) {

          int exactMatch = getExactPrefixMatchCount(
            requestUriParts, Splitter.on('/').omitEmptyStrings().split(resourceModel.getPath()));

          // When there are multiple matches present, the following precedence order is used -
          // 1. template path that has highest exact prefix match with the url is chosen.
          // 2. template path has the maximum groups is chosen.
          // 3. finally, template path that has the longest length is chosen.
          if (exactMatch > maxExactMatch) {
            maxExactMatch = exactMatch;
            maxGroupMatch = groupMatch;
            maxPatternLength = resourceModel.getPath().length();

            matchedDestinations.clear();
            matchedDestinations.add(destination);
          } else if (exactMatch == maxExactMatch && groupMatch >= maxGroupMatch) {
            if (groupMatch > maxGroupMatch || resourceModel.getPath().length() > maxPatternLength) {
              maxGroupMatch = groupMatch;
              maxPatternLength = resourceModel.getPath().length();
              matchedDestinations.clear();
            }
            matchedDestinations.add(destination);
          }
        }
      }
    }

    if (matchedDestinations.size() > 1) {
      throw new IllegalStateException(String.format("Multiple matched handlers found for request uri %s", requestUri));
    } else if (matchedDestinations.size() == 1) {
      return matchedDestinations.get(0);
    }
    return null;
  }

  /**
   * @return the number of path components that match from left to right.
   */
  private int getExactPrefixMatchCount(Iterable first, Iterable second) {
    int count = 0;
    for (Iterator fit = first.iterator(), sit = second.iterator(); fit.hasNext() && sit.hasNext(); ) {
      if (fit.next().equals(sit.next())) {
        ++count;
      } else {
        break;
      }
    }
    return count;
  }

  @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);
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy