RestMethodHandler.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */
package org.waarp.gateway.kernel.rest;

import com.fasterxml.jackson.databind.node.ArrayNode;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.multipart.FileUpload;
import org.waarp.common.logging.SysErrLogger;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.utility.WaarpStringUtils;
import org.waarp.gateway.kernel.exception.HttpForbiddenRequestException;
import org.waarp.gateway.kernel.exception.HttpIncorrectRequestException;
import org.waarp.gateway.kernel.exception.HttpInvalidAuthenticationException;
import org.waarp.gateway.kernel.exception.HttpMethodNotAllowedRequestException;
import org.waarp.gateway.kernel.exception.HttpNotFoundRequestException;
import org.waarp.gateway.kernel.rest.DataModelRestMethodHandler.COMMAND_TYPE;
import org.waarp.gateway.kernel.rest.HttpRestHandler.METHOD;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * Rest Method handler (used by Http Rest Handler)
 */
public abstract class RestMethodHandler {
  /**
   * Internal Logger
   */
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(RestMethodHandler.class);
  private static final METHOD[] METHOD_0_LENGTH = new METHOD[0];

  protected final String name;
  protected final String path;
  protected final Set<METHOD> methods;
  protected final boolean isBodyJsonDecode;
  protected final RestConfiguration restConfiguration;

  /**
   * @param name name associated with this Method Handler (to enable
   *     some
   *     HashMap or Enum
   *     classification)
   * @param path associated base Path
   * @param isBodyJsonDecode Is this method Handler using a Json as
   *     Body
   * @param config the associated configuration
   * @param method the associated methods
   */
  protected RestMethodHandler(final String name, final String path,
                              final boolean isBodyJsonDecode,
                              final RestConfiguration config,
                              final METHOD... method) {
    this.name = name;
    this.path = path;
    methods = new HashSet<HttpRestHandler.METHOD>();
    setMethods(method);
    setMethods(METHOD.OPTIONS);
    this.isBodyJsonDecode = isBodyJsonDecode;
    restConfiguration = config;
  }

  protected final void setMethods(final METHOD... method) {
    Collections.addAll(methods, method);
  }

  /**
   * Will assign the intersection of both set of Methods
   *
   * @param selectedMethods the selected Methods among available
   * @param validMethod the validMethod for this handler
   */
  protected final void setIntersectionMethods(final METHOD[] selectedMethods,
                                              final METHOD... validMethod) {
    final Set<METHOD> set = new HashSet<METHOD>();
    Collections.addAll(set, validMethod);
    final Set<METHOD> set2 = new HashSet<METHOD>();
    Collections.addAll(set2, selectedMethods);
    set.retainAll(set2);
    final METHOD[] methodsToSet = set.toArray(METHOD_0_LENGTH);
    setMethods(methodsToSet);
  }

  public final String getName() {
    return name;
  }

  public final String getPath() {
    return path;
  }

  /**
   * @param method
   *
   * @return True if the Method is valid for this Handler
   */
  public final boolean isMethodIncluded(final METHOD method) {
    return methods.contains(method);
  }

  /**
   * Check the session (arguments, result) vs handler correctness, called
   * before
   * any BODY elements but after URI
   * and HEADER.
   *
   * @param handler
   * @param arguments
   * @param result
   *
   * @throws HttpForbiddenRequestException
   */
  public abstract void checkHandlerSessionCorrectness(HttpRestHandler handler,
                                                      RestArgument arguments,
                                                      RestArgument result)
      throws HttpForbiddenRequestException;

  /**
   * Get a new Http Uploaded File from BODY
   *
   * @param handler
   * @param data
   * @param arguments
   * @param result
   *
   * @throws HttpIncorrectRequestException
   */
  public abstract void getFileUpload(HttpRestHandler handler, FileUpload data,
                                     RestArgument arguments,
                                     RestArgument result)
      throws HttpIncorrectRequestException;

  /**
   * Get data from BODY (supposedly a Json)
   *
   * @param handler
   * @param body
   * @param arguments
   * @param result
   *
   * @return the object related to BODY decoding
   *
   * @throws HttpIncorrectRequestException
   */
  public abstract Object getBody(HttpRestHandler handler, ByteBuf body,
                                 RestArgument arguments, RestArgument result)
      throws HttpIncorrectRequestException;

  /**
   * Called when all Data were passed to the handler
   *
   * @param handler
   * @param arguments
   * @param result
   * @param body
   *
   * @throws HttpIncorrectRequestException
   * @throws HttpNotFoundRequestException
   */
  public abstract void endParsingRequest(HttpRestHandler handler,
                                         RestArgument arguments,
                                         RestArgument result, Object body)
      throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
             HttpNotFoundRequestException;

  /**
   * Called when an exception occurs
   *
   * @param handler
   * @param arguments
   * @param result
   * @param body
   * @param exception
   *
   * @return the status to used in sendReponse
   */
  public HttpResponseStatus handleException(final HttpRestHandler handler,
                                            final RestArgument arguments,
                                            final RestArgument result,
                                            final Object body,
                                            final Exception exception) {
    if (exception instanceof HttpInvalidAuthenticationException) {
      result.setResult(HttpResponseStatus.UNAUTHORIZED);
      return HttpResponseStatus.UNAUTHORIZED;
    } else if (exception instanceof HttpForbiddenRequestException) {
      result.setResult(HttpResponseStatus.FORBIDDEN);
      return HttpResponseStatus.FORBIDDEN;
    } else if (exception instanceof HttpIncorrectRequestException) {
      result.setResult(HttpResponseStatus.BAD_REQUEST);
      return HttpResponseStatus.BAD_REQUEST;
    } else if (exception instanceof HttpMethodNotAllowedRequestException) {
      result.setResult(HttpResponseStatus.METHOD_NOT_ALLOWED);
      return HttpResponseStatus.METHOD_NOT_ALLOWED;
    } else if (exception instanceof HttpNotFoundRequestException) {
      result.setResult(HttpResponseStatus.NOT_FOUND);
      return HttpResponseStatus.NOT_FOUND;
    } else {
      result.setResult(HttpResponseStatus.INTERNAL_SERVER_ERROR);
      return HttpResponseStatus.INTERNAL_SERVER_ERROR;
    }
  }

  /**
   * Send a response (correct or not)
   *
   * @param handler
   * @param ctx
   * @param arguments
   * @param result
   * @param body
   * @param status
   *
   * @return The ChannelFuture if this response will need the channel to be
   *     closed, else null
   */
  public abstract ChannelFuture sendResponse(HttpRestHandler handler,
                                             ChannelHandlerContext ctx,
                                             RestArgument arguments,
                                             RestArgument result, Object body,
                                             HttpResponseStatus status);

  protected final ChannelFuture sendOptionsResponse(
      final HttpRestHandler handler, final ChannelHandlerContext ctx,
      final RestArgument result, final HttpResponseStatus status) {
    final String list = result.getAllowOption();
    final String answer = result.toString();
    final ByteBuf buffer =
        Unpooled.wrappedBuffer(answer.getBytes(WaarpStringUtils.UTF8));
    final HttpResponse response = handler.getResponse(buffer);
    if (status == HttpResponseStatus.UNAUTHORIZED) {
      return ctx.writeAndFlush(response);
    }
    response.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
    response.headers().add(HttpHeaderNames.REFERER, handler.getRequest().uri());
    response.headers().add(HttpHeaderNames.ALLOW, list);
    logger.debug("Msg ready");
    final ChannelFuture future = ctx.writeAndFlush(response);
    if (handler.isWillClose()) {
      SysErrLogger.FAKE_LOGGER.sysout(
          "Will close session in RestMethodHandler");
      return future;
    }
    return null;
  }

  /**
   * Options command that all handler should implement
   *
   * @param handler
   * @param arguments
   * @param result
   */
  protected void optionsCommand(final HttpRestHandler handler,
                                final RestArgument arguments,
                                final RestArgument result) {
    result.setCommand(COMMAND_TYPE.OPTIONS);
    final METHOD[] realmethods = METHOD.values();
    final boolean[] allMethods = new boolean[realmethods.length];
    for (final METHOD methoditem : methods) {
      allMethods[methoditem.ordinal()] = true;
    }
    StringBuilder allow = null;
    for (int i = 0; i < allMethods.length; i++) {
      if (allMethods[i]) {
        if (allow == null) {
          allow = new StringBuilder(realmethods[i].name());
        } else {
          allow.append(',').append(realmethods[i].name());
        }
      }
    }
    if (allow != null) {
      result.addOptions(allow.toString(), path, getDetailedAllow());
    }
  }

  /**
   * @return the detail of the method handler
   */
  protected abstract ArrayNode getDetailedAllow();

  /**
   * @return the isBodyJson
   */
  public final boolean isBodyJsonDecoded() {
    return isBodyJsonDecode;
  }
}