/**
   * 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 interceptors Iterable of interceptors.
   * @param urlRewriter URL re-writer.
   * @param exceptionHandler Exception handler
   */
  public MicroserviceMetadata(
      Iterable<? extends Object> handlers,
      Iterable<? extends Interceptor> interceptors,
      URLRewriter urlRewriter,
      ExceptionHandler exceptionHandler) {
    // Store the handlers to call init and destroy on all handlers.
    this.handlers = ImmutableList.copyOf(handlers);
    this.interceptors = ImmutableList.copyOf(interceptors);
    this.urlRewriter = urlRewriter;

    for (Object 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.isAnnotationPresent(PostConstruct.class)
            || method.isAnnotationPresent(PreDestroy.class)) {
          continue;
        }

        if (Modifier.isPublic(method.getModifiers()) && isHttpMethodAvailable(method)) {
          String relativePath = "";
          if (method.getAnnotation(Path.class) != null) {
            relativePath = method.getAnnotation(Path.class).value();
          }
          String absolutePath = String.format("%s/%s", basePath, relativePath);
          patternRouter.add(
              absolutePath,
              new HttpResourceModel(absolutePath, method, handler, new ExceptionHandler()));
        } else {
          log.trace(
              "Not adding method {}({}) to path routing like. "
                  + "HTTP calls will not be routed to this method",
              method.getName(),
              method.getParameterTypes());
        }
      }
    }
  }
/**
 * MicroserviceMetadata handles the http request. HttpResourceHandler looks up all Jax-rs
 * annotations in classes and dispatches to appropriate method on receiving requests.
 */
public final class MicroserviceMetadata {

  private static final Logger log = LoggerFactory.getLogger(MicroserviceMetadata.class);

  private final PatternPathRouterWithGroups<HttpResourceModel> patternRouter =
      PatternPathRouterWithGroups.create();
  private final Iterable<Object> handlers;
  private final Iterable<Interceptor> interceptors;
  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 interceptors Iterable of interceptors.
   * @param urlRewriter URL re-writer.
   * @param exceptionHandler Exception handler
   */
  public MicroserviceMetadata(
      Iterable<? extends Object> handlers,
      Iterable<? extends Interceptor> interceptors,
      URLRewriter urlRewriter,
      ExceptionHandler exceptionHandler) {
    // Store the handlers to call init and destroy on all handlers.
    this.handlers = ImmutableList.copyOf(handlers);
    this.interceptors = ImmutableList.copyOf(interceptors);
    this.urlRewriter = urlRewriter;

    for (Object 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.isAnnotationPresent(PostConstruct.class)
            || method.isAnnotationPresent(PreDestroy.class)) {
          continue;
        }

        if (Modifier.isPublic(method.getModifiers()) && isHttpMethodAvailable(method)) {
          String relativePath = "";
          if (method.getAnnotation(Path.class) != null) {
            relativePath = method.getAnnotation(Path.class).value();
          }
          String absolutePath = String.format("%s/%s", basePath, relativePath);
          patternRouter.add(
              absolutePath,
              new HttpResourceModel(absolutePath, method, handler, new ExceptionHandler()));
        } else {
          log.trace(
              "Not adding method {}({}) to path routing like. "
                  + "HTTP calls will not be routed to this method",
              method.getName(),
              method.getParameterTypes());
        }
      }
    }
  }

  private boolean isHttpMethodAvailable(Method method) {
    return method.isAnnotationPresent(GET.class)
        || method.isAnnotationPresent(PUT.class)
        || method.isAnnotationPresent(POST.class)
        || method.isAnnotationPresent(DELETE.class);
  }

  /**
   * 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.
   * @throws HandlerException If URL rewriting fails
   */
  public HttpMethodInfoBuilder getDestinationMethod(HttpRequest request, HttpResponder responder)
      throws HandlerException {
    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()));
      }
    }

    String acceptHeaderStr = request.headers().get(HttpHeaders.Names.ACCEPT);
    List<String> acceptHeader =
        (acceptHeaderStr != null)
            ? Arrays.asList(acceptHeaderStr.split("\\s*,\\s*"))
                .stream()
                .map(mediaType -> mediaType.split("\\s*;\\s*")[0])
                .collect(Collectors.toList())
            : null;

    String contentTypeHeaderStr = request.headers().get(HttpHeaders.Names.CONTENT_TYPE);
    // Trim specified charset since UTF-8 is assumed
    String contentTypeHeader =
        (contentTypeHeaderStr != null) ? contentTypeHeaderStr.split("\\s*;\\s*")[0] : null;

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

      List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>>
          routableDestinations = patternRouter.getDestinations(path);

      List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> matchedDestinations =
          getMatchedDestination(routableDestinations, request.getMethod(), path);

      if (!matchedDestinations.isEmpty()) {
        PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel> matchedDestination =
            matchedDestinations
                .stream()
                .filter(
                    matchedDestination1 -> {
                      return matchedDestination1
                              .getDestination()
                              .matchConsumeMediaType(contentTypeHeader)
                          && matchedDestination1
                              .getDestination()
                              .matchProduceMediaType(acceptHeader);
                    })
                .findFirst()
                .get();
        HttpResourceModel httpResourceModel = matchedDestination.getDestination();

        // Call preCall method of handler interceptors.
        boolean terminated = false;
        ServiceMethodInfo serviceMethodInfo =
            new ServiceMethodInfo(
                httpResourceModel.getMethod().getDeclaringClass().getName(),
                httpResourceModel.getMethod());
        for (Interceptor interceptor : interceptors) {
          if (!interceptor.preCall(request, responder, serviceMethodInfo)) {
            // 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, interceptors, request, serviceMethodInfo);
          return HttpMethodInfoBuilder.getInstance()
              .httpResourceModel(httpResourceModel)
              .httpRequest(request)
              .httpResponder(responder)
              .requestInfo(
                  matchedDestination.getGroupNameValues(), contentTypeHeader, acceptHeader);
        }
      } else if (!routableDestinations.isEmpty()) {
        // 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 (NoSuchElementException ex) {
      throw new HandlerException(
          HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE,
          String.format("Problem accessing: %s. Reason: Unsupported Media Type", request.getUri()),
          ex);
    }
    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 List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>>
      getMatchedDestination(
          List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>>
              routableDestinations,
          HttpMethod targetHttpMethod,
          String requestUri) {

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

    for (PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel> 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);
          }
        }
      }
    }
    return matchedDestinations;
  }

  /** @return the number of path components that match from left to right. */
  private int getExactPrefixMatchCount(Iterable<String> first, Iterable<String> second) {
    int count = 0;
    for (Iterator<String> fit = first.iterator(), sit = second.iterator();
        fit.hasNext() && sit.hasNext(); ) {
      if (fit.next().equals(sit.next())) {
        ++count;
      } else {
        break;
      }
    }
    return count;
  }
}
  /**
   * 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.
   * @throws HandlerException If URL rewriting fails
   */
  public HttpMethodInfoBuilder getDestinationMethod(HttpRequest request, HttpResponder responder)
      throws HandlerException {
    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()));
      }
    }

    String acceptHeaderStr = request.headers().get(HttpHeaders.Names.ACCEPT);
    List<String> acceptHeader =
        (acceptHeaderStr != null)
            ? Arrays.asList(acceptHeaderStr.split("\\s*,\\s*"))
                .stream()
                .map(mediaType -> mediaType.split("\\s*;\\s*")[0])
                .collect(Collectors.toList())
            : null;

    String contentTypeHeaderStr = request.headers().get(HttpHeaders.Names.CONTENT_TYPE);
    // Trim specified charset since UTF-8 is assumed
    String contentTypeHeader =
        (contentTypeHeaderStr != null) ? contentTypeHeaderStr.split("\\s*;\\s*")[0] : null;

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

      List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>>
          routableDestinations = patternRouter.getDestinations(path);

      List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> matchedDestinations =
          getMatchedDestination(routableDestinations, request.getMethod(), path);

      if (!matchedDestinations.isEmpty()) {
        PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel> matchedDestination =
            matchedDestinations
                .stream()
                .filter(
                    matchedDestination1 -> {
                      return matchedDestination1
                              .getDestination()
                              .matchConsumeMediaType(contentTypeHeader)
                          && matchedDestination1
                              .getDestination()
                              .matchProduceMediaType(acceptHeader);
                    })
                .findFirst()
                .get();
        HttpResourceModel httpResourceModel = matchedDestination.getDestination();

        // Call preCall method of handler interceptors.
        boolean terminated = false;
        ServiceMethodInfo serviceMethodInfo =
            new ServiceMethodInfo(
                httpResourceModel.getMethod().getDeclaringClass().getName(),
                httpResourceModel.getMethod());
        for (Interceptor interceptor : interceptors) {
          if (!interceptor.preCall(request, responder, serviceMethodInfo)) {
            // 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, interceptors, request, serviceMethodInfo);
          return HttpMethodInfoBuilder.getInstance()
              .httpResourceModel(httpResourceModel)
              .httpRequest(request)
              .httpResponder(responder)
              .requestInfo(
                  matchedDestination.getGroupNameValues(), contentTypeHeader, acceptHeader);
        }
      } else if (!routableDestinations.isEmpty()) {
        // 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 (NoSuchElementException ex) {
      throw new HandlerException(
          HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE,
          String.format("Problem accessing: %s. Reason: Unsupported Media Type", request.getUri()),
          ex);
    }
    return null;
  }