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