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;
      }
   }
}