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

co.cask.http.internal.HttpResourceModel Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2017 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.internal;

import co.cask.http.ExceptionHandler;
import co.cask.http.HttpHandler;
import co.cask.http.HttpResponder;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.QueryStringDecoder;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;

/**
 * HttpResourceModel contains information needed to handle Http call for a given path. Used as a destination in
 * {@code PatternPathRouterWithGroups} to route URI paths to right Http end points.
 */
public final class HttpResourceModel {

  private static final Set> SUPPORTED_PARAM_ANNOTATIONS =
    Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PathParam.class, QueryParam.class, HeaderParam.class)));

  private final Set httpMethods;
  private final String path;
  private final Method method;
  private final HttpHandler handler;
  private final List, ParameterInfo>> paramsInfo;
  private final ExceptionHandler exceptionHandler;

  /**
   * Construct a resource model with HttpMethod, method that handles httprequest, Object that contains the method.
   *
   * @param httpMethods Set of http methods that is handled by the resource.
   * @param path path associated with this model.
   * @param method handler that handles the http request.
   * @param handler instance {@code HttpHandler}.
   */
  public HttpResourceModel(Set httpMethods, String path, Method method, HttpHandler handler,
                           ExceptionHandler exceptionHandler) {
    this.httpMethods = httpMethods;
    this.path = path;
    this.method = method;
    this.handler = handler;
    this.paramsInfo = createParametersInfos(method);
    this.exceptionHandler = exceptionHandler;
  }

  /**
   * @return httpMethods.
   */
  public Set getHttpMethod() {
    return httpMethods;
  }

  /**
   * @return path associated with this model.
   */
  public String getPath() {
    return path;
  }

  /**
   * @return handler method that handles an http end-point.
   */
  public Method getMethod() {
    return method;
  }

  /**
   * @return instance of {@code HttpHandler}.
   */
  public HttpHandler getHttpHandler() {
    return handler;
  }

  /**
   * Handle http Request.
   *
   * @param request  HttpRequest to be handled.
   * @param responder HttpResponder to write the response.
   * @param groupValues Values needed for the invocation.
   */
  @SuppressWarnings("unchecked")
  public HttpMethodInfo handle(HttpRequest request,
                               HttpResponder responder, Map groupValues) throws Exception {
    //TODO: Refactor group values.
    try {
      if (httpMethods.contains(request.method())) {
        //Setup args for reflection call
        Object [] args = new Object[paramsInfo.size()];

        int idx = 0;
        for (Map, ParameterInfo> info : paramsInfo) {
          if (info.containsKey(PathParam.class)) {
            args[idx] = getPathParamValue(info, groupValues);
          }
          if (info.containsKey(QueryParam.class)) {
            args[idx] = getQueryParamValue(info, request.uri());
          }
          if (info.containsKey(HeaderParam.class)) {
            args[idx] = getHeaderParamValue(info, request);
          }
          idx++;
        }

        return new HttpMethodInfo(method, handler, responder, args, exceptionHandler);
      } else {
        //Found a matching resource but could not find the right HttpMethod so return 405
        throw new HandlerException(HttpResponseStatus.METHOD_NOT_ALLOWED, String.format
          ("Problem accessing: %s. Reason: Method Not Allowed", request.uri()));
      }
    } catch (Throwable e) {
      throw new HandlerException(HttpResponseStatus.INTERNAL_SERVER_ERROR,
                                 String.format("Error in executing request: %s %s", request.method(),
                                               request.uri()), e);
    }
  }

  @Override
  public String toString() {
    return "HttpResourceModel{" +
      "httpMethods=" + httpMethods +
      ", path='" + path + '\'' +
      ", method=" + method +
      ", handler=" + handler +
      '}';
  }

  @SuppressWarnings("unchecked")
  private Object getPathParamValue(Map, ParameterInfo> annotations,
                                   Map groupValues) throws Exception {
    ParameterInfo info = (ParameterInfo) annotations.get(PathParam.class);
    PathParam pathParam = info.getAnnotation();
    String value = groupValues.get(pathParam.value());
    if (value == null) {
      throw new IllegalArgumentException("Could not resolve value for path parameter " + pathParam.value());
    }
    return info.convert(value);
  }

  @SuppressWarnings("unchecked")
  private Object getQueryParamValue(Map, ParameterInfo> annotations,
                                    String uri) throws Exception {
    ParameterInfo> info = (ParameterInfo>) annotations.get(QueryParam.class);
    QueryParam queryParam = info.getAnnotation();
    List values = new QueryStringDecoder(uri).parameters().get(queryParam.value());

    return (values == null) ? info.convert(defaultValue(annotations)) : info.convert(values);
  }

  @SuppressWarnings("unchecked")
  private Object getHeaderParamValue(Map, ParameterInfo> annotations,
                                     HttpRequest request) throws Exception {
    ParameterInfo> info = (ParameterInfo>) annotations.get(HeaderParam.class);
    HeaderParam headerParam = info.getAnnotation();
    String headerName = headerParam.value();
    boolean hasHeader = request.headers().contains(headerName);
    return hasHeader ? info.convert(request.headers().getAll(headerName)) : info.convert(defaultValue(annotations));
  }

  /**
   * Returns a List of String created based on the {@link DefaultValue} if it is presented in the annotations Map.
   *
   * @return a List of String or an empty List if {@link DefaultValue} is not presented
   */
  private List defaultValue(Map, ParameterInfo> annotations) {
    ParameterInfo defaultInfo = annotations.get(DefaultValue.class);
    if (defaultInfo == null) {
      return Collections.emptyList();
    }

    DefaultValue defaultValue = defaultInfo.getAnnotation();
    return Collections.singletonList(defaultValue.value());
  }

  /**
   * Gathers all parameters' annotations for the given method, starting from the third parameter.
   */
  private List, ParameterInfo>> createParametersInfos(Method method) {
    if (method.getParameterTypes().length <= 2) {
      return Collections.emptyList();
    }

    List, ParameterInfo>> result = new ArrayList<>();
    Type[] parameterTypes = method.getGenericParameterTypes();
    Annotation[][] parameterAnnotations = method.getParameterAnnotations();

    for (int i = 2; i < parameterAnnotations.length; i++) {
      Annotation[] annotations = parameterAnnotations[i];
      Map, ParameterInfo> paramAnnotations = new IdentityHashMap<>();

      for (Annotation annotation : annotations) {
        Class annotationType = annotation.annotationType();
        ParameterInfo parameterInfo;

        if (PathParam.class.isAssignableFrom(annotationType)) {
          parameterInfo = ParameterInfo.create(annotation,
                                               ParamConvertUtils.createPathParamConverter(parameterTypes[i]));
        } else if (QueryParam.class.isAssignableFrom(annotationType)) {
          parameterInfo = ParameterInfo.create(annotation,
                                               ParamConvertUtils.createQueryParamConverter(parameterTypes[i]));
        } else if (HeaderParam.class.isAssignableFrom(annotationType)) {
          parameterInfo = ParameterInfo.create(annotation,
                                               ParamConvertUtils.createHeaderParamConverter(parameterTypes[i]));
        } else {
          parameterInfo = ParameterInfo.create(annotation, null);
        }

        paramAnnotations.put(annotationType, parameterInfo);
      }

      // Must have either @PathParam, @QueryParam or @HeaderParam, but not two or more.
      int presence = 0;
      for (Class annotationClass : paramAnnotations.keySet()) {
        if (SUPPORTED_PARAM_ANNOTATIONS.contains(annotationClass)) {
          presence++;
        }
      }
      if (presence != 1) {
        throw new IllegalArgumentException(
          String.format("Must have exactly one annotation from %s for parameter %d in method %s",
                        SUPPORTED_PARAM_ANNOTATIONS, i, method));
      }

      result.add(Collections.unmodifiableMap(paramAnnotations));
    }

    return Collections.unmodifiableList(result);
  }

  /**
   * A container class to hold information about a handler method parameters.
   */
  private static final class ParameterInfo {
    private final Annotation annotation;
    private final Converter converter;

    static  ParameterInfo create(Annotation annotation, @Nullable Converter converter) {
      return new ParameterInfo<>(annotation, converter);
    }

    private ParameterInfo(Annotation annotation, @Nullable Converter converter) {
      this.annotation = annotation;
      this.converter = converter;
    }

    @SuppressWarnings("unchecked")
     V getAnnotation() {
      return (V) annotation;
    }

    Object convert(T input) throws Exception {
      return (converter == null) ? null : converter.convert(input);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy