# RequestArguments.java

    /*
    ** Module   : RequestArguments.java
    ** Abstract : APIs to manage the request arguments from the HTTP servlet request.
    **
    ** Copyright (c) 2019-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- -------------------------------Description--------------------------------
    ** 001 CA  20190614 First version.
    ** 002 CA  20190628 Added TABLE output support and Content-Type (gzip,deflate) aware.
    ** 003 CA  20190710 Moved the HTTP body read APIs to LegacyServiceHandler.
    ** 004 CA  20191119 Added support for before-table.
    ** 005 CA  20200427 Fixed TABLE and added DATASET support; misc improvements.
    **     CA  20200514 Refactoring to allow common code for SOAP services support.
    **     CA  20200528 Added APIs for REST extent arguments (not implemented yet).
    ** 006 CA  20210304 Fixed date, datetime and datetimetz literal parsing.
    ** 007 SBI 20230216 Added rest.request[property] logic to get target properties of the http request object.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.rest;

    import java.io.*;
    import java.lang.invoke.MethodHandle;
    import java.lang.invoke.MethodHandles;
    import java.lang.reflect.*;
    import java.math.*;
    import java.util.*;

    import javax.servlet.http.*;

    import com.goldencode.p2j.directory.Base64;
    import com.goldencode.p2j.util.*;

    /**
     * Helper class to resolve the arguments for a REST call.
     */
    abstract class RequestArguments
    extends ServiceArgumentsParser
    {
       /**
        * Parse an extent argument.
        *
        * @param    body
        *           The HTTP body.
        * @param    request
        *           The request payload.
        * @param    idx
        *           The argument's index.
        * @param    source
        *           The arguments's source.
        * @param    type
        *           The arguments's type.
        * @param    extent
        *           The arguments's length.
        *
        * @return   An array with the argument's values.
        */
       @Override
       protected Object[] parseExtentArgument(String             body,
                                              HttpServletRequest request,
                                              int                idx,
                                              String             source,
                                              String             type,
                                              int                extent)
       {
          // TODO: add this
          UnimplementedFeature.missing("REST extent arguments");

          return null;
       }

       /**
        * Assign the given argument to the specified value.
        *
        * @param    idx
        *           The argument's index.
        * @param    bdt
        *           The argument's {@link BaseDataType} instance (may be <code>null</code>).
        * @param    sval
        *           The string representation of this argument.
        */
       @Override
       protected void assignArgument(int idx, BaseDataType bdt, String sval)
       throws RequestArgumentError
       {
          switch (bdt.getClass().getSimpleName())
          {
             case "integer":
                ((integer) bdt).assign(new BigInteger(sval));
                break;

             case "int64":
                ((int64) bdt).assign(new BigInteger(sval));
                break;

             case "decimal":
                ((decimal) bdt).assign(new BigDecimal(sval));
                break;

             case "logical":
                ((logical) bdt).assign(Boolean.parseBoolean(sval));
                break;

             case "raw":
                ((BinaryData) bdt).assign(Base64.safeBase64ToByteArray(sval));
                break;

             case "date":
                date d = date.parseIso8601(sval);
                if (d == null)
                {
                   notAValue(bdt, sval);
                }
                ((date) bdt).assign(d);
                break;

             case "datetime":
                date dt = date.parseIso8601(sval);
                if (dt == null)
                {
                   notAValue(bdt, sval);
                }
                ((datetime) bdt).assign(dt);
                break;

             case "datetimetz":
                date dtz = date.parseIso8601(sval);
                if (dtz == null)
                {
                   notAValue(bdt, sval);
                }
                ((datetimetz) bdt).assign(dtz);
                break;

             default:
                bdt.assign(sval);
                break;
          }
       }

       /**
        * Parse the argument, by interpreting the request.
        *
        * @param    body
        *           The request body.
        * @param    source
        *           The parameter's encoded source.
        * @param    request
        *           The request payload.
        *
        * @return   The resolved argument.
        */
       @Override
       protected String parseArgumentInt(String body, String source, HttpServletRequest request)
       throws IOException
       {
          if (source.startsWith("${"))
          {
             source = source.substring(2);
          }
          if (source.endsWith("}"))
          {
             source = source.substring(0, source.length() - 1);
          }

          switch (source)
          {
             case "rest.verb":
                return request.getMethod();

             case "http.body":
                return body;

             case "http.headers":
                // TODO: correct serialization format?
                return readHeaders(request);

             case "http.uristring":
                // the full url
                return request.getRequestURL().toString();
          }

          // these are a JSON with [ { "theName" : "", "theValue" : "val" }], for each and every
          // String get*() method in the class
          if (source.equals("rest.context.httpservletrequest"))
          {
             return readAttributes(request);
          }
          else if (source.equals("rest.context.httpservletresponse"))
          {
             return null;
          }
          else if (source.equals("rest.context.servletcontext"))
          {
             return readAttributes(request.getServletContext());
          }
          else if (source.equals("rest.context.servletconfig"))
          {
             return "[ { \"theName\" : \"ServletName\", \"theValue\" : \"FWDRestServlet\" } ]";
          }

          if (source.startsWith("http.header["))
          {
             String val = RestHandler.extractVal(source);

             return request.getHeader(val);
          }
          else if (source.startsWith("http.request["))
          {
             String property = RestHandler.extractVal(source);

             return getProperty(request, property, String.class);
          }

          if (source.startsWith("rest.queryparam[") || source.startsWith("rest.formparam["))
          {
             String param = RestHandler.extractVal(source);
             String val = request.getParameter(param);
             String contentType = request.getHeader("Content-type");
             if ((val == null && body.isEmpty()) ||
                 !"application/x-www-form-urlencoded".equals(contentType))
             {
                return val;
             }

             // parse the body
             String[] params = body.split("&");
             for (int i = 0; i < params.length; i++)
             {
                int idx = params[i].indexOf('=');
                String pname = idx >= 0 ? params[i].substring(0, idx) : "";
                String pval = idx >= 0 ? params[i].substring(idx + 1) : "";
                if (pname.equalsIgnoreCase(param))
                {
                   return pval;
                }
             }
          }

          if (source.startsWith("rest.cookieparam["))
          {
             String val = RestHandler.extractVal(source);

             if (request.getCookies() != null)
             {
                for (Cookie cookie : request.getCookies())
                {
                   if (cookie.getName().equalsIgnoreCase(val))
                   {
                      return cookie.getValue();
                   }
                }
             }

             return null;
          }

          if (source.startsWith("rest.pathparam["))
          {
             String path = RestHandler.extractVal(source);

             String param = path.substring(path.indexOf(';') + 1);
             path = path.substring(0, path.indexOf(';'));

             String[] paths = RestHandler.getPaths(path);
             int paramIdx = -1;
             for (int i = 0; i < paths.length; i++)
             {
                if (paths[i].equals("{" + param + "}"))
                {
                   paramIdx = i;
                   break;
                }
             }

             String rpath = request.getPathInfo();
             String[] rpaths = RestHandler.getPaths(rpath);

             return rpaths[paramIdx];
          }

          return null;
       }

       /**
        * Read the headers from the request.
        *
        * @param    request
        *           The request payload.
        *
        * @return   The headers.
        */
       private String readHeaders(HttpServletRequest request)
       {
          // TODO: what is the correct format?

          String s = "";
          Enumeration<String> headers = request.getHeaderNames();
          while (headers.hasMoreElements())
          {
             String header = headers.nextElement();

             s += header + "=" + request.getHeader(header) + System.getProperty("line.separator");
          }

          return s;
       }

       /**
        * Read the fields from the specified instance.
        *
        * @param    ref
        *           The instance to resolve fields from.
        *
        * @return   A JSON structure with all the {@link String} fields which have a getter.
        */
       private String readAttributes(Object ref)
       {
          if (ref == null)
          {
             return null;
          }

          String s = null;

          Class<?> cls = ref.getClass();
          for (Method m : cls.getMethods())
          {
             String mname = m.getName();
             if (mname.startsWith("get")    &&
                 m.getParameterCount() == 0 &&
                 m.getReturnType() == String.class)
             {
                if (s == null)
                {
                   s = "";
                }

                try
                {
                   String name = mname.substring(3);
                   String val = (String) m.invoke(ref);

                   if (val != null)
                   {
                      s = s + (s.isEmpty() ? "" : ", ") +
                          "{ \"theName\" : \"" + name + "\", \"theValue\" : \"" + val + "\" }";
                   }
                   else
                   {
                      s = s + (s.isEmpty() ? "" : ", ") + "{ \"theName\" : \"" + name + "\" }";
                   }
                }
                catch (Exception e)
                {
                   // ignore
                }
             }
          }

          return s == null ? null : "[ " + s + " ]";
       }

       /**
        * Gets the object's property of the given type.
        *
        * @param    ref
        *           The given object
        * @param    property
        *           The target property's name
        * @param    type
        *           The target property's type
        *
        * @return   The value of the object's property or null
        */
       <T> T getProperty(Object ref, String property, Class<T> type)
       {
          if (ref == null)
          {
             return null;
          }
          Class<?> cls = ref.getClass();

          MethodHandles.Lookup lookup = MethodHandles.lookup();
          MethodHandle mh;
          try
          {
             mh = lookup.findGetter(cls, property, type);
          }
          catch(NoSuchFieldException | IllegalAccessException ex)
          {
             return null;
          }
          try
          {
             return (T) mh.invokeExact(ref);
          }
          catch(Throwable ex)
          {
             return null;
          }
       }
    }