RestArgument.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.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.QueryStringEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.waarp.common.crypto.HmacSha256;
import org.waarp.common.exception.InvalidArgumentException;
import org.waarp.common.json.JsonHandler;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.role.RoleDefault;
import org.waarp.common.role.RoleDefault.ROLE;
import org.waarp.common.utility.ParametersChecker;
import org.waarp.gateway.kernel.exception.HttpIncorrectRequestException;
import org.waarp.gateway.kernel.exception.HttpInvalidAuthenticationException;
import org.waarp.gateway.kernel.rest.DataModelRestMethodHandler.COMMAND_TYPE;
import org.waarp.gateway.kernel.rest.HttpRestHandler.METHOD;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

/**
 * Rest object that contains all arguments or answers once all tasks are
 * over:</br>
 * - ARG_HASBODY, ARG_METHOD, ARG_PATH, ARG_BASEPATH, ARGS_SUBPATH,
 * ARG_X_AUTH_KEY, ARG_X_AUTH_USER,
 * ARG_X_AUTH_TIMESTAMP, ARG_X_AUTH_ROLE: root elements (query only)</br>
 * - ARG_METHOD, ARG_PATH, ARG_BASEPATH, ARGS_SUBPATH, ARG_X_AUTH_USER,
 * JSON_STATUSMESSAGE, JSON_STATUSCODE,
 * JSON_COMMAND: root elements (answer only)</br>
 * - ARGS_URI: uri elements (query only)</br>
 * - ARGS_HEADER: header elements (query only)</br>
 * - ARG_COOKIE: cookie elements</br>
 * - ARGS_BODY: body elements (query only)</br>
 * - ARGS_ANSWER: answer part (answer only)</br>
 */
public class RestArgument {
  /**
   * Internal Logger
   */
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(RestArgument.class);

  public enum REST_GROUP {
    /**
     * arguments.path(ARGS_URI) main entry for URI arguments
     */
    ARGS_URI("uri"),
    /**
     * arguments.path(ARGS_HEADER) main entry for HEADER arguments
     */
    ARGS_HEADER("header"),
    /**
     * arguments.path(ARGS_COOKIE) main entry for COOKIE arguments
     */
    ARGS_COOKIE("cookie"),
    /**
     * arguments.path(ARGS_BODY) main entry for BODY arguments
     */
    ARGS_BODY("body"),
    /**
     * arguments.path(ARGS_ANSWER) main entry for ANSWER arguments
     */
    ARGS_ANSWER("answer");

    public final String group;

    REST_GROUP(final String group) {
      this.group = group;
    }
  }

  public enum REST_ROOT_FIELD {
    /**
     * arguments.path(ARG_PATH) = uri path
     */
    ARG_PATH("path"),
    /**
     * arguments.path(ARG_BASEPATH).asText() = uri base path
     */
    ARG_BASEPATH("base"),
    /**
     * arguments.path(ARGS_SUBPATH) main entry for SUB-PATH arguments<br>
     * arguments.path(ARGS_SUBPATH).elements() for an iterator or .get(x)
     * for
     * xth SUB-PATH argument
     */
    ARGS_SUBPATH("subpath"),
    /**
     * arguments.path(ARG_METHOD).asText() = method identified
     */
    ARG_METHOD("X-method"),
    /**
     * arguments.path(ARG_HASBODY).asBoolean() = true if the body has
     * content
     */
    ARG_HASBODY("hasBody"),
    /**
     * arguments.path(ARG_X_AUTH_KEY).asText() = Key used
     */
    ARG_X_AUTH_KEY("X-Auth-Key"),
    /**
     * arguments.path(ARG_X_AUTH_KEY).asText() = Key used
     */
    ARG_X_AUTH_USER("X-Auth-User"),
    /**
     * Internal Key used (not to be passed through wire)
     */
    ARG_X_AUTH_INTERNALKEY("X-Auth-InternalKey"),
    /**
     * arguments.path(ARG_X_AUTH_TIMESTAMP).asText() = Timestamp in ISO 8601
     * format
     */
    ARG_X_AUTH_TIMESTAMP("X-Auth-Timestamp"),
    /**
     * arguments.path(ARG_X_AUTH_ROLE).asInt() = Role used
     */
    ARG_X_AUTH_ROLE("X-Auth-Role"), JSON_STATUSCODE("code"),
    JSON_STATUSMESSAGE("message"), JSON_COMMAND("command"),
    JSON_DETAIL("detail");

    public final String field;

    REST_ROOT_FIELD(final String field) {
      this.field = field;
    }
  }

  public enum REST_FIELD {
    JSON_RESULT("result"), JSON_PATH("path"), JSON_JSON("body"),
    X_DETAILED_ALLOW("DetailedAllow"), X_ALLOW_URIS("UriAllowed"),
    JSON_ID("_id");

    public final String field;

    REST_FIELD(final String field) {
      this.field = field;
    }
  }

  public enum DATAMODEL {
    JSON_COUNT("count"), JSON_RESULTS("results"), JSON_FILTER("filter"),
    JSON_LIMIT("limit");

    public final String field;

    DATAMODEL(final String field) {
      this.field = field;
    }
  }

  final ObjectNode arguments;

  /**
   * Create a RestArgument
   *
   * @param emptyArgument might be null, but might be also already
   *     initialized with some values
   */
  public RestArgument(final ObjectNode emptyArgument) {
    if (emptyArgument == null) {
      arguments = JsonHandler.createObjectNode();
    } else {
      arguments = emptyArgument;
    }
  }

  /**
   * Clean all internal values
   */
  public final void clean() {
    arguments.removeAll();
  }

  /**
   * Set values according to the request URI
   *
   * @param request
   */
  public final void setRequest(final HttpRequest request) {
    arguments.put(REST_ROOT_FIELD.ARG_HASBODY.field,
                  request instanceof FullHttpRequest &&
                  ((FullHttpRequest) request).content() !=
                  Unpooled.EMPTY_BUFFER);
    arguments.put(REST_ROOT_FIELD.ARG_METHOD.field, request.method().name());
    final QueryStringDecoder decoderQuery =
        new QueryStringDecoder(request.uri());
    final String path = decoderQuery.path();
    arguments.put(REST_ROOT_FIELD.ARG_PATH.field, path);
    // compute path main uri
    String basepath = path;
    int pos = basepath.indexOf('/');
    if (pos >= 0) {
      if (pos == 0) {
        final int pos2 = basepath.indexOf('/', 1);
        if (pos2 < 0) {
          basepath = basepath.substring(1);
        } else {
          basepath = basepath.substring(1, pos2);
        }
      } else {
        basepath = basepath.substring(0, pos);
      }
    }
    arguments.put(REST_ROOT_FIELD.ARG_BASEPATH.field, basepath);
    // compute sub path args
    if (pos == 0) {
      pos = path.indexOf('/', 1);
    }
    if (pos >= 0) {
      int pos2 = path.indexOf('/', pos + 1);
      if (pos2 > 0) {
        final ArrayNode array =
            arguments.putArray(REST_ROOT_FIELD.ARGS_SUBPATH.field);
        while (pos2 > 0) {
          array.add(path.substring(pos + 1, pos2));
          pos = pos2;
          pos2 = path.indexOf('/', pos + 1);
        }
      }
      pos2 = path.indexOf('?', pos + 1);
      if (pos2 > 0 && pos2 > pos + 1) {
        ArrayNode array =
            (ArrayNode) arguments.get(REST_ROOT_FIELD.ARGS_SUBPATH.field);
        if (array == null) {
          array = arguments.putArray(REST_ROOT_FIELD.ARGS_SUBPATH.field);
        }
        array.add(path.substring(pos + 1, pos2));
      } else {
        final String last = path.substring(pos + 1);
        if (!last.isEmpty()) {
          ArrayNode array =
              (ArrayNode) arguments.get(REST_ROOT_FIELD.ARGS_SUBPATH.field);
          if (array == null) {
            array = arguments.putArray(REST_ROOT_FIELD.ARGS_SUBPATH.field);
          }
          array.add(last);
        }
      }
    }
    final Map<String, List<String>> map = decoderQuery.parameters();
    final ObjectNode node = arguments.putObject(REST_GROUP.ARGS_URI.group);
    for (final Entry<String, List<String>> entry : map.entrySet()) {
      final String key = entry.getKey();
      try {
        ParametersChecker.checkSanityString(
            entry.getValue().toArray(ParametersChecker.ZERO_ARRAY_STRING));
        if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_KEY.field)) {
          arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_KEY.field,
                        entry.getValue().get(0));
          continue;
        }
        if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_USER.field)) {
          arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_USER.field,
                        entry.getValue().get(0));
          continue;
        }
        if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field)) {
          arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field,
                        entry.getValue().get(0));
          continue;
        }
        final List<String> list = entry.getValue();
        if (list.size() > 1) {
          final ArrayNode array = node.putArray(key);
          for (final String val : entry.getValue()) {
            array.add(val);
          }
        } else if (list.isEmpty()) {
          node.putNull(key);
        } else {
          // 1
          node.put(key, list.get(0));
        }
      } catch (final InvalidArgumentException e) {
        logger.error("Arguments incompatible with Security: " + entry.getKey(),
                     e);
      }
    }
  }

  /**
   * Set X_AUTH_USER, Method, Path, Basepath and Cookie from source
   *
   * @param source
   */
  public final void setFromArgument(final RestArgument source) {
    if (source.arguments.has(REST_ROOT_FIELD.ARG_X_AUTH_USER.field)) {
      arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_USER.field,
                    source.arguments.get(REST_ROOT_FIELD.ARG_X_AUTH_USER.field)
                                    .asText());
    }
    if (source.arguments.has(REST_ROOT_FIELD.ARG_METHOD.field)) {
      arguments.put(REST_ROOT_FIELD.ARG_METHOD.field,
                    source.arguments.get(REST_ROOT_FIELD.ARG_METHOD.field)
                                    .asText());
    }
    if (source.arguments.has(REST_ROOT_FIELD.ARG_PATH.field)) {
      arguments.put(REST_ROOT_FIELD.ARG_PATH.field,
                    source.arguments.get(REST_ROOT_FIELD.ARG_PATH.field)
                                    .asText());
    }
    if (source.arguments.has(REST_ROOT_FIELD.ARG_BASEPATH.field)) {
      arguments.put(REST_ROOT_FIELD.ARG_BASEPATH.field,
                    source.arguments.get(REST_ROOT_FIELD.ARG_BASEPATH.field)
                                    .asText());
    }
    if (source.arguments.has(REST_ROOT_FIELD.ARGS_SUBPATH.field)) {
      arguments.putArray(REST_ROOT_FIELD.ARGS_SUBPATH.field).addAll(
          (ArrayNode) source.arguments.get(REST_ROOT_FIELD.ARGS_SUBPATH.field));
    }
    if (source.arguments.has(REST_GROUP.ARGS_URI.group)) {
      arguments.putObject(REST_GROUP.ARGS_URI.group).setAll(
          (ObjectNode) source.arguments.get(REST_GROUP.ARGS_URI.group));
    }
    if (source.arguments.has(REST_GROUP.ARGS_COOKIE.group)) {
      arguments.putObject(REST_GROUP.ARGS_COOKIE.group).setAll(
          (ObjectNode) source.arguments.get(REST_GROUP.ARGS_COOKIE.group));
    }

    logger.debug("DEBUG: {}\n {}", arguments, source);
  }

  /**
   * @return the full Path of the URI
   */
  public final String getUri() {
    return arguments.path(REST_ROOT_FIELD.ARG_PATH.field).asText();
  }

  /**
   * @return the base Path of the URI (first item between '/')
   */
  public final String getBaseUri() {
    return arguments.path(REST_ROOT_FIELD.ARG_BASEPATH.field).asText();
  }

  /**
   * @return An iterator of JsonNode, which values can be retrieved by
   *     item.asText()
   */
  public final Iterator<JsonNode> getSubUri() {
    return arguments.path(REST_ROOT_FIELD.ARGS_SUBPATH.field).elements();
  }

  public final int getSubUriSize() {
    return arguments.path(REST_ROOT_FIELD.ARGS_SUBPATH.field).size();
  }

  public final void addSubUriToUriArgs(final String name, final int rank) {
    final ObjectNode node = getUriArgs();
    final JsonNode elt =
        arguments.path(REST_ROOT_FIELD.ARGS_SUBPATH.field).get(rank);
    if (elt != null) {
      node.set(name, elt);
    }
  }

  public final void addIdToUriArgs() {
    addSubUriToUriArgs(REST_FIELD.JSON_ID.field, 0);
  }

  public final JsonNode getId() {
    return getUriArgs().path(REST_FIELD.JSON_ID.field);
  }

  public static JsonNode getId(final ObjectNode node) {
    return node.path(REST_FIELD.JSON_ID.field);
  }

  public final long getLimitFromUri() {
    return getUriArgs().path(DATAMODEL.JSON_LIMIT.field).asLong(100);
  }

  public final String getXAuthKey() {
    return arguments.path(REST_ROOT_FIELD.ARG_X_AUTH_KEY.field).asText();
  }

  public final String getXAuthUser() {
    return arguments.path(REST_ROOT_FIELD.ARG_X_AUTH_USER.field).asText();
  }

  public final String getXAuthTimestamp() {
    return arguments.path(REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field).asText();
  }

  public final void setXAuthRole(final RoleDefault role) {
    arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_ROLE.field, role.getRoleAsByte());
  }

  public final ROLE getXAuthRole() {
    final byte role =
        (byte) arguments.get(REST_ROOT_FIELD.ARG_X_AUTH_ROLE.field).asInt();
    return ROLE.fromByte(role);
  }

  /**
   * @return The ObjectNode containing all couples key/value
   */
  public final ObjectNode getUriArgs() {
    JsonNode node = arguments.path(REST_GROUP.ARGS_URI.group);
    if (node == null || node.isMissingNode()) {
      node = arguments.putObject(REST_GROUP.ARGS_URI.group);
    }
    return (ObjectNode) node;
  }

  /**
   * @return the method or null
   */
  public final METHOD getMethod() {
    final String text =
        arguments.path(REST_ROOT_FIELD.ARG_METHOD.field).asText();
    if (ParametersChecker.isEmpty(text)) {
      return METHOD.TRACE;
    }
    try {
      return METHOD.valueOf(text);
    } catch (final Exception e) {
      return METHOD.TRACE;
    }
  }

  /**
   * set values from Header into arguments.path(ARGS_HEADER)
   *
   * @throws HttpIncorrectRequestException
   */
  public final void setHeaderArgs(final List<Entry<String, String>> list) {
    ObjectNode node = (ObjectNode) arguments.get(REST_GROUP.ARGS_HEADER.group);
    if (node == null || node.isMissingNode()) {
      node = arguments.putObject(REST_GROUP.ARGS_HEADER.group);
    }
    for (final Entry<String, String> entry : list) {
      try {
        ParametersChecker.checkSanityString(entry.getValue());
        final String key = entry.getKey();
        if (!key.equals(HttpHeaderNames.COOKIE.toString())) {
          if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_KEY.field)) {
            arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_KEY.field,
                          entry.getValue());
            continue;
          }
          if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_USER.field)) {
            arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_USER.field,
                          entry.getValue());
            continue;
          }
          if (key.equalsIgnoreCase(
              REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field)) {
            arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field,
                          entry.getValue());
            continue;
          }
          node.put(key, entry.getValue());
        }
      } catch (final InvalidArgumentException e) {
        logger.error("Arguments incompatible with Security: " + entry.getKey(),
                     e);
      }
    }
  }

  /**
   * set values from Header into arguments.path(ARGS_HEADER)
   *
   * @throws HttpIncorrectRequestException
   */
  public final void setHeaderArgs(
      final Iterator<Entry<CharSequence, CharSequence>> iterator) {
    ObjectNode node = (ObjectNode) arguments.get(REST_GROUP.ARGS_HEADER.group);
    if (node == null || node.isMissingNode()) {
      node = arguments.putObject(REST_GROUP.ARGS_HEADER.group);
    }
    while (iterator.hasNext()) {
      final Entry<CharSequence, CharSequence> entry = iterator.next();
      try {
        ParametersChecker.checkSanityString(entry.getValue().toString());
        final String key = entry.getKey().toString();
        if (!key.equals(HttpHeaderNames.COOKIE.toString())) {
          if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_KEY.field)) {
            arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_KEY.field,
                          entry.getValue().toString());
            continue;
          }
          if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_USER.field)) {
            arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_USER.field,
                          entry.getValue().toString());
            continue;
          }
          if (key.equalsIgnoreCase(
              REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field)) {
            arguments.put(REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field,
                          entry.getValue().toString());
            continue;
          }
          node.put(key, entry.getValue().toString());
        }
      } catch (final InvalidArgumentException e) {
        logger.error("Arguments incompatible with Security: " + entry.getKey(),
                     e);
      }
    }
  }

  /**
   * @return The ObjectNode containing all couples key/value
   */
  public final ObjectNode getHeaderArgs() {
    JsonNode node = arguments.path(REST_GROUP.ARGS_HEADER.group);
    if (node == null || node.isMissingNode()) {
      node = arguments.putObject(REST_GROUP.ARGS_HEADER.group);
    }
    return (ObjectNode) node;
  }

  /**
   * set method From URI
   */
  public final void methodFromUri() {
    final JsonNode node = arguments.path(REST_GROUP.ARGS_URI.group)
                                   .path(REST_ROOT_FIELD.ARG_METHOD.field);
    if (!node.isMissingNode()) {
      // override
      arguments.put(REST_ROOT_FIELD.ARG_METHOD.field, node.asText());
    }
  }

  /**
   * set method From Header
   */
  public final void methodFromHeader() {
    final JsonNode node = arguments.path(REST_GROUP.ARGS_HEADER.group)
                                   .path(REST_ROOT_FIELD.ARG_METHOD.field);
    if (!node.isMissingNode()) {
      // override
      arguments.put(REST_ROOT_FIELD.ARG_METHOD.field, node.asText());
    }
  }

  /**
   * set values from Cookies into arguments.path(ARGS_COOKIE)
   */
  public final void setCookieArgs(final String cookieString) {
    final Set<Cookie> cookies;
    if (cookieString == null) {
      cookies = Collections.emptySet();
    } else {
      cookies = ServerCookieDecoder.LAX.decode(cookieString);
    }
    if (!cookies.isEmpty()) {
      final ObjectNode node = arguments.putObject(REST_GROUP.ARGS_COOKIE.group);
      for (final Cookie cookie : cookies) {
        try {
          ParametersChecker.checkSanityString(cookie.value());
          node.put(cookie.name(), cookie.value());
        } catch (final InvalidArgumentException e) {
          logger.error("Arguments incompatible with Security: " + cookie.name(),
                       e);
        }
      }
    }
  }

  /**
   * @return The ObjectNode containing all couples key/value
   */
  public final ObjectNode getCookieArgs() {
    JsonNode node = arguments.path(REST_GROUP.ARGS_COOKIE.group);
    if (node == null || node.isMissingNode()) {
      node = arguments.putObject(REST_GROUP.ARGS_COOKIE.group);
    }
    return (ObjectNode) node;
  }

  /**
   * @return The ObjectNode containing all couples key/value
   */
  public final ObjectNode getBody() {
    JsonNode node = arguments.path(REST_GROUP.ARGS_BODY.group);
    if (node == null || node.isMissingNode()) {
      node = arguments.putObject(REST_GROUP.ARGS_BODY.group);
    }
    return (ObjectNode) node;
  }

  /**
   * @return The ObjectNode containing all couples key/value
   */
  public final ObjectNode getAnswer() {
    JsonNode node = arguments.path(REST_GROUP.ARGS_ANSWER.group);
    if (node == null || node.isMissingNode()) {
      node = arguments.putObject(REST_GROUP.ARGS_ANSWER.group);
    }
    return (ObjectNode) node;
  }

  public final void addAnswer(final ObjectNode node) {
    getAnswer().setAll(node);
  }

  public final void setResult(final HttpResponseStatus status) {
    arguments.put(REST_ROOT_FIELD.JSON_STATUSMESSAGE.field,
                  status.reasonPhrase());
    arguments.put(REST_ROOT_FIELD.JSON_STATUSCODE.field, status.code());
  }

  /**
   * @return the Http Status code
   */
  public final int getStatusCode() {
    return arguments.path(REST_ROOT_FIELD.JSON_STATUSCODE.field).asInt();
  }

  /**
   * @return the Http Status message according to the Http Status code
   */
  public final String getStatusMessage() {
    return arguments.path(REST_ROOT_FIELD.JSON_STATUSMESSAGE.field).asText();
  }

  public final void setDetail(final String detail) {
    arguments.put(REST_ROOT_FIELD.JSON_DETAIL.field, detail);
  }

  /**
   * @return the detail information on error (mainly)
   */
  public final String getDetail() {
    return arguments.path(REST_ROOT_FIELD.JSON_DETAIL.field).asText();
  }

  public final void setCommand(final COMMAND_TYPE command) {
    arguments.put(REST_ROOT_FIELD.JSON_COMMAND.field, command.name());
  }

  public final void setCommand(final String cmd) {
    arguments.put(REST_ROOT_FIELD.JSON_COMMAND.field, cmd);
  }

  /**
   * @return the COMMAND field, to be transformed either into COMMAND_TYPE or
   *     ACTIONS_TYPE
   */
  public final String getCommandField() {
    return arguments.path(REST_ROOT_FIELD.JSON_COMMAND.field).asText();
  }

  /**
   * @return the COMMAND_TYPE but might be null if not found or if of
   *     ACTIONS_TYPE
   */
  public final COMMAND_TYPE getCommand() {
    final String cmd =
        arguments.path(REST_ROOT_FIELD.JSON_COMMAND.field).asText();
    if (ParametersChecker.isNotEmpty(cmd)) {
      try {
        return COMMAND_TYPE.valueOf(cmd);
      } catch (final Exception e) {
        return null;
      }
    } else {
      return null;
    }
  }

  /**
   * @param filter the filter used in multi get
   */
  public final void addFilter(ObjectNode filter) {
    if (filter == null) {
      filter = JsonHandler.createObjectNode();
    }
    getAnswer().putObject(DATAMODEL.JSON_FILTER.field).setAll(filter);
  }

  /**
   * @return the filter used in multi get
   */
  public final ObjectNode getFilter() {
    return (ObjectNode) getAnswer().path(DATAMODEL.JSON_FILTER.field);
  }

  /**
   * @return the array of results (in DataModel multi get)
   */
  public final ArrayNode getResults() {
    JsonNode node = getAnswer().path(DATAMODEL.JSON_RESULTS.field);
    if (node == null || node.isMissingNode()) {
      node = getAnswer().putArray(DATAMODEL.JSON_RESULTS.field);
    }
    return (ArrayNode) node;
  }

  /**
   * @param result added to the array of results (in DataModel multi
   *     get)
   */
  public final void addResult(final ObjectNode result) {
    getResults().add(result);
  }

  /**
   * @param count added to answer if > 0
   * @param limit added to answer
   */
  public final void addCountLimit(final long count, final long limit) {
    final ObjectNode node = getAnswer();
    if (count >= 0) {
      node.put(DATAMODEL.JSON_COUNT.field, count);
    }
    node.put(DATAMODEL.JSON_LIMIT.field, limit);
  }

  /**
   * @return the count of element (-1 if not found)
   */
  public final long getCount() {
    return getAnswer().path(DATAMODEL.JSON_COUNT.field).asLong(-1);
  }

  public final long getLimit() {
    return getAnswer().path(DATAMODEL.JSON_LIMIT.field).asLong(100);
  }

  /**
   * Add options in answer
   *
   * @param allow
   * @param path
   * @param detailedAllow
   */
  public final void addOptions(final String allow, final String path,
                               final ArrayNode detailedAllow) {
    final ObjectNode node = getAnswer();
    node.put(HttpHeaderNames.ALLOW.toString(), allow);
    node.put(REST_FIELD.X_ALLOW_URIS.field, path);
    if (detailedAllow != null) {
      node.putArray(REST_FIELD.X_DETAILED_ALLOW.field).addAll(detailedAllow);
    }
  }

  public final String getAllowOption() {
    return getAnswer().path(HttpHeaderNames.ALLOW.toString()).asText();
  }

  public final String getAllowUrisOption() {
    return getAnswer().path(REST_FIELD.X_ALLOW_URIS.field).asText();
  }

  public final ArrayNode getDetailedAllowOption() {
    final JsonNode node = getAnswer().path(REST_FIELD.X_DETAILED_ALLOW.field);
    if (node.isMissingNode()) {
      return JsonHandler.createArrayNode();
    } else {
      return (ArrayNode) node;
    }
  }

  /**
   * The encoder is completed with extra necessary URI part containing
   * ARG_X_AUTH_TIMESTAMP & ARG_X_AUTH_KEY
   *
   * @param hmacSha256 SHA-256 key to create the signature
   * @param encoder
   * @param user might be null
   * @param extraKey might be null
   *
   * @return an array of 2 value in order ARG_X_AUTH_TIMESTAMP and
   *     ARG_X_AUTH_KEY
   *
   * @throws HttpInvalidAuthenticationException if the computation of
   *     the
   *     authentication failed
   */
  public static String[] getBaseAuthent(final HmacSha256 hmacSha256,
                                        final QueryStringEncoder encoder,
                                        final String user,
                                        final String extraKey)
      throws HttpInvalidAuthenticationException {
    final QueryStringDecoder decoderQuery =
        new QueryStringDecoder(encoder.toString());
    final Map<String, List<String>> map = decoderQuery.parameters();
    final TreeMap<String, String> treeMap = new TreeMap<String, String>();
    for (final Entry<String, List<String>> entry : map.entrySet()) {
      final String keylower = entry.getKey().toLowerCase();
      final List<String> values = entry.getValue();
      if (values != null && !values.isEmpty()) {
        final String last = values.get(values.size() - 1);
        treeMap.put(keylower, last);
      }
    }
    final DateTime date = new DateTime();
    treeMap.put(REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field.toLowerCase(),
                date.toString());
    if (user != null) {
      treeMap.put(REST_ROOT_FIELD.ARG_X_AUTH_USER.field.toLowerCase(), user);
    }
    try {
      final String key =
          computeKey(hmacSha256, extraKey, treeMap, decoderQuery.path());
      return new String[] { date.toString(), key };
    } catch (final Exception e) {
      throw new HttpInvalidAuthenticationException(e);
    }

  }

  /**
   * Check Time only (no signature)
   *
   * @param maxInterval
   *
   * @throws HttpInvalidAuthenticationException
   */
  public final void checkTime(final long maxInterval)
      throws HttpInvalidAuthenticationException {
    final DateTime dateTime = new DateTime();
    final String date = getXAuthTimestamp();
    if (ParametersChecker.isNotEmpty(date)) {
      final DateTime received = DateTime.parse(date);
      if (maxInterval > 0) {
        final Duration duration = new Duration(received, dateTime);
        if (Math.abs(duration.getMillis()) >= maxInterval) {
          throw new HttpInvalidAuthenticationException(
              "timestamp is not compatible with the maximum delay allowed");
        }
      }
    } else if (maxInterval > 0) {
      throw new HttpInvalidAuthenticationException(
          "timestamp absent while required");
    }
  }

  /**
   * This implementation of authentication is as follow: if X_AUTH is included
   * in the URI or Header<br>
   * 0) Check that timestamp is correct (|curtime - timestamp| < maxinterval)
   * from ARG_X_AUTH_TIMESTAMP, if
   * maxInterval is 0, not mandatory<br>
   * 1) Get all URI args (except ARG_X_AUTH_KEY itself, but including
   * timestamp), lowered case, in alphabetic
   * order<br>
   * 2) Add an extra Key if not null (from ARG_X_AUTH_INTERNALKEY)<br>
   * 3) Compute an hash (SHA-1 or SHA-256)<br>
   * 4) Compare this hash with ARG_X_AUTH_KEY<br>
   *
   * @param hmacSha256 SHA-256 key to create the signature
   * @param extraKey will be added as ARG_X_AUTH_INTERNALKEY might be
   *     null
   * @param maxInterval ARG_X_AUTH_TIMESTAMP will be tested if value >
   *     0
   *
   * @throws HttpInvalidAuthenticationException if the authentication
   *     failed
   */
  public final void checkBaseAuthent(final HmacSha256 hmacSha256,
                                     final String extraKey,
                                     final long maxInterval)
      throws HttpInvalidAuthenticationException {
    final TreeMap<String, String> treeMap = new TreeMap<String, String>();
    final String argPath = getUri();
    final ObjectNode arguri = getUriArgs();
    if (arguri == null) {
      throw new HttpInvalidAuthenticationException("Not enough argument");
    }
    final Iterator<Entry<String, JsonNode>> iterator = arguri.fields();
    final DateTime dateTime = new DateTime();
    DateTime received = null;
    while (iterator.hasNext()) {
      final Entry<String, JsonNode> entry = iterator.next();
      final String key = entry.getKey();
      if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_KEY.field)) {
        continue;
      }
      final JsonNode values = entry.getValue();
      if (key.equalsIgnoreCase(REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field)) {
        received = DateTime.parse(values.asText());
      }
      final String keylower = key.toLowerCase();
      if (values != null) {
        final String val;
        if (values.isArray()) {
          final JsonNode jsonNode = values.get(values.size() - 1);
          val = jsonNode.asText();
        } else {
          val = values.asText();
        }
        treeMap.put(keylower, val);
      }
    }
    if (received == null) {
      final String date = getXAuthTimestamp();
      received = DateTime.parse(date);
      treeMap.put(REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field.toLowerCase(),
                  date);
    }
    final String user = getXAuthUser();
    if (ParametersChecker.isNotEmpty(user)) {
      treeMap.put(REST_ROOT_FIELD.ARG_X_AUTH_USER.field.toLowerCase(), user);
    }
    if (maxInterval > 0 && received != null) {
      final Duration duration = new Duration(received, dateTime);
      if (Math.abs(duration.getMillis()) >= maxInterval) {
        throw new HttpInvalidAuthenticationException(
            "timestamp is not compatible with the maximum delay allowed");
      }
    } else if (maxInterval > 0) {
      throw new HttpInvalidAuthenticationException(
          "timestamp absent while required");
    }
    final String key = computeKey(hmacSha256, extraKey, treeMap, argPath);
    if (!key.equalsIgnoreCase(getXAuthKey())) {
      throw new HttpInvalidAuthenticationException(
          "Invalid Authentication Key");
    }

  }

  /**
   * @param hmacSha256 SHA-256 key to create the signature
   * @param extraKey might be null
   * @param treeMap
   * @param argPath
   *
   * @throws HttpInvalidAuthenticationException
   */
  protected static String computeKey(final HmacSha256 hmacSha256,
                                     final String extraKey,
                                     final TreeMap<String, String> treeMap,
                                     final String argPath)
      throws HttpInvalidAuthenticationException {
    final Set<String> keys = treeMap.keySet();
    final StringBuilder builder = new StringBuilder(argPath);
    if (!keys.isEmpty() || extraKey != null) {
      builder.append('?');
    }
    boolean first = true;
    for (final String keylower : keys) {
      if (first) {
        first = false;
      } else {
        builder.append('&');
      }
      builder.append(keylower).append('=').append(treeMap.get(keylower));
    }
    if (extraKey != null) {
      if (!keys.isEmpty()) {
        builder.append('&');
      }
      builder.append(REST_ROOT_FIELD.ARG_X_AUTH_INTERNALKEY.field).append('=')
             .append(extraKey);
    }
    try {
      return hmacSha256.cryptToHex(builder.toString());
    } catch (final Exception e) {
      throw new HttpInvalidAuthenticationException(e);
    }
  }

  @Override
  public String toString() {
    return JsonHandler.writeAsString(arguments);
  }

  public final String prettyPrint() {
    return JsonHandler.prettyPrint(arguments);
  }

  public static ObjectNode fillDetailedAllow(final METHOD method,
                                             final String path,
                                             final String command,
                                             final ObjectNode body,
                                             final JsonNode result) {
    final ObjectNode node = JsonHandler.createObjectNode();
    final ObjectNode node2 = node.putObject(method.name());
    node2.put(REST_FIELD.JSON_PATH.field, '/' + path);
    node2.put(REST_ROOT_FIELD.JSON_COMMAND.field, command);
    if (body != null) {
      node2.set(REST_FIELD.JSON_JSON.field, body);
    }
    if (result != null) {
      node2.set(REST_GROUP.ARGS_ANSWER.group, result);
    }
    return node;
  }
}