# SoapRequestParser.java

    /*
    ** Module   : SoapRequestParser.java
    ** Abstract : APIs to parse the SOAP request arguments.
    **
    ** Copyright (c) 2020-2021, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20200518 First version.
    **     CA  20200528 Fixes for extent parameters, BEFORE-TABLE and error management.
    ** 002 CA  20210304 Fixed date, datetime and datetimetz literal parsing.
    **     CA  20211013 DATASET parameters have the table table indexes, too, at the WSDL schema.
    **     CA  20211112 Refactored to allow transfer of the DATASET or TABLE via XML.  Added support for
    **                  DATASET-HANDLE and TABLE-HANDLE.
    ** 003 RNC 20241128 Implemented logic to recognize 'nil' attribute.
    */

    /*
    ** 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.soap;

    import java.io.*;
    import java.lang.reflect.*;
    import java.math.*;
    import java.util.*;

    import javax.servlet.http.*;
    import javax.wsdl.*;
    import javax.xml.bind.*;
    import javax.xml.namespace.*;
    import javax.xml.stream.*;

    import org.apache.axiom.om.*;
    import org.apache.axiom.om.util.*;
    import org.apache.axis2.databinding.utils.*;

    import com.goldencode.p2j.directory.Base64;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.rest.*;
    import com.goldencode.p2j.soap.WsdlConfig.*;
    import com.goldencode.p2j.util.*;

    /**
     * Parse the service input arguments from the SOAP envelope (body or header).
     */
    public class SoapRequestParser
    extends ServiceArgumentsParser
    {
       /** The SOAP body element which encodes the request. */
       private final OMElement soapRequest;

       /** The map of service input parameters, (FWD-style) as defined at the WSDL operation. */
       private final Map<String, LegacyServiceParameter> params = new HashMap<>();

       /** The schema definition for the service input parameters, as defined at the schema. */
       private final Map<String, BaseSchemaType> soapParams;

       /** The list of service input parameters (FWD-style) as defined at the WSDL operation. */
       private final LegacyServiceParameter[] serviceParameters;

       /**
        * Initialize this instance, to parse the input parameters for the SOAP operation.
        *
        * @param    params
        *           The output parameters for the SOAP operation.
        * @param    soapRequest
        *           The SOAP request body.
        * @param    soapOperation
        *           The SOAP operation as defined in the WSDL.
        * @param    wsdlConfig
        *           The WSDL configuration.
        */
       public SoapRequestParser(LegacyServiceParameter[] params,
                                OMElement                soapRequest,
                                Operation                soapOperation,
                                WsdlConfig               wsdlConfig)
       {
          this.soapRequest = soapRequest;
          this.serviceParameters = params;

          for (LegacyServiceParameter lsp : params)
          {
             this.params.put(lsp.name().toLowerCase(), lsp);
          }

          soapParams = wsdlConfig.resolveParameters(soapOperation);
       }

       /**
        * 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)
       throws RequestArgumentError
       {
          LegacyServiceParameter lsp = serviceParameters[idx];
          BaseSchemaType bst = soapParams.get(lsp.name());

          Class<?> cls = BaseDataType.fromTypeName(type);

          List<BaseDataType> vals = new ArrayList<>();
          Iterator<OMElement> iter = getSoapParameters(source);
          while (iter.hasNext())
          {
             OMElement el = iter.next();

             BaseDataType bdt = BaseDataType.generateUnknown(cls);
             bdt.assign(createArgument(bst.xsdType, cls, el.getText()));

             vals.add(bdt);
          }

          int length = extent == SourceNameMapper.DYNAMIC_EXTENT ? vals.size() : extent;
          if (extent != SourceNameMapper.DYNAMIC_EXTENT && length != vals.size() && !vals.isEmpty())
          {
             // must match
             throw new RequestArgumentError("extent not match");
          }
          Object arg = Array.newInstance(cls, length);
          for (int i = 0; i < vals.size(); i++)
          {
             Array.set(arg, i, vals.get(i));
          }
          for (int i = vals.size(); i < length; i++)
          {
             Array.set(arg, i, BaseDataType.generateUnknown(cls));
          }

          return (Object[]) arg;
       }

       /**
        * Assign the given argument to the specified value.
        * <p>
        * This will assign the <code>bdt</code> instance, depending on the argument's configured XSD type.
        *
        * @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
       {
          LegacyServiceParameter lsp = serviceParameters[idx];
          BaseSchemaType bst = soapParams.get(lsp.name());

          QName nilQName = new QName("http://www.w3.org/2001/XMLSchema-instance", "nil");
          String nilValue = getSoapParameter(lsp.source()).getAttributeValue(nilQName);

          if (nilValue == null || nilValue.equalsIgnoreCase("false"))
          {
             bdt.assign(createArgument(bst.xsdType, bdt.getClass(), sval));
          }
          else if (nilValue.equalsIgnoreCase("true"))
          {
             bdt.setUnknown();
          }
          else
          {
             throw new RequestArgumentError("Invalid boolean value: " + nilValue + " (10915)", true);
          }
       }

       /**
        * 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
       {
          OMElement parEl = getSoapParameter(source);
          LegacyServiceParameter lsp = getLegacyParameter(source);

          switch (lsp.type().toUpperCase())
          {
             case "DATASET":
             case "DATASET-HANDLE":
             case "TABLE":
             case "TABLE-HANDLE":
                return source;

             default:
                return parEl == null ? null : parEl.getText();
          }
       }

       /**
        * Load the specified table.
        *
        * @param    content
        *           The table definition.
        *
        * @return   A {@link TableWrapper} instance to be passed as argument to the remote call.
        */
       @Override
       protected TableWrapper loadTable(String content)
       throws IOException,
              RequestArgumentError
       {
          OMElement parEl = getSoapParameter(content);
          LegacyServiceParameter lsp = getLegacyParameter(content);

          if (parEl == null)
          {
             // the table is not specified, create an 'empty' node
             try
             {
                parEl = AXIOMUtil.stringToOM("<" + lsp.name() + "/>");
             }
             catch (XMLStreamException e)
             {
                throw new RuntimeException(e);
             }
          }
          else if ("TABLE-HANDLE".equalsIgnoreCase(lsp.type()))
          {
             // in this case, the XML looks like this:
             // root element: the TABLE-HANDLE parameter name
             //    > first child: -> TempTable node
             //       > first child: the schema
             //       > second child: the table with its rows (the schema node must be moved here)
             // we need to move the schema node to the proper child and use that XML

             OMElement schemaEl = null;
             OMElement tableEl = null;
             Iterator<OMElement> iter1 = parEl.getChildrenWithLocalName("TempTable");
             if (iter1.hasNext())
             {
                Iterator<OMElement> iter = iter1.next().getChildElements();
                while (iter.hasNext())
                {
                   OMElement child = iter.next();
                   if (child.getNamespace() != null &&
                       child.getNamespace().getNamespaceURI().equals("http://www.w3.org/2001/XMLSchema"))
                   {
                      schemaEl = child;
                   }
                   else
                   {
                      if (tableEl != null)
                      {
                         tableEl = null;
                         break;
                      }
                      tableEl = child;
                   }
                }
             }

             if (schemaEl != null && tableEl != null)
             {
                schemaEl.detach();
                Iterator<OMElement> iter = tableEl.getChildElements();
                List<OMElement> rows = new ArrayList<>();
                while (iter.hasNext())
                {
                   OMElement row = iter.next();
                   rows.add(row);
                }
                for (OMElement row : rows)
                {
                   row.detach();
                }

                // schema must be first!
                tableEl.addChild(schemaEl);
                for (OMElement row : rows)
                {
                   tableEl.addChild(row);
                }
                parEl = tableEl;
             }
             else
             {
                // TODO: error ?
             }
          }

          SoapHandler.removeNamespaces(parEl);
          String xmlTable = parEl.toString();

          TableWrapper wrapper = new TableWrapper(lsp.input(), lsp.output(), false);
          wrapper.setXmlTable(xmlTable);
          wrapper.setAsXml(true);

          return wrapper;
       }

       /**
        * Create a new {@link DatasetWrapper}, so that the transfer is made via XML.
        *
        * @param    name
        *           The dataset name.
        * @param    input
        *           The INPUT mode.
        * @param    output
        *           The OUTPUT mode.
        * @param    asHandle
        *           Flag indicating if this is a DATASET-HANDLE parameter.
        *
        * @return   A new {@link DatasetWrapper} for XML transfer.
        */
       @Override
       protected DatasetWrapper createDataset(String name, boolean input, boolean output, boolean asHandle)
       {
          DataSetSchemaType dst = name == null || name.isEmpty() ? null : (DataSetSchemaType) soapParams.get(name);

          DatasetWrapper wrapper = new DatasetWrapper(input, output, asHandle);
          wrapper.setAsXml(true);
          wrapper.setUseBeforeImage(dst != null && dst.useBeforeImage);

          return wrapper;
       }

       /**
        * Create a new {@link TableWrapper}, so that the transfer is made via XML.
        *
        * @param    name
        *           The table name.
        * @param    input
        *           The INPUT mode.
        * @param    output
        *           The OUTPUT mode.
        * @param    asHandle
        *           Flag indicating if this is a DATASET-HANDLE parameter.
        *
        * @return   A new {@link TableWrapper} for XML transfer.
        */
       @Override
       protected TableWrapper createTable(String name, boolean input, boolean output, boolean asHandle)
       {
          TableSchemaType tst = name == null || name.isEmpty() ? null : (TableSchemaType) soapParams.get(name);

          TableWrapper wrapper = new TableWrapper(input, output, asHandle);
          wrapper.setAsXml(true);

          return wrapper;
       }

       /**
        * Load the specified dataset.
        *
        * @param    content
        *           The dataset definition.
        *
        * @return   A {@link DatasetWrapper} instance to be passed as argument to the remote call.
        */
       @Override
       protected DatasetWrapper loadDataSet(String content)
       throws IOException,
              RequestArgumentError
       {
          OMElement parEl = getSoapParameter(content);
          LegacyServiceParameter lsp = getLegacyParameter(content);

          if (parEl == null)
          {
             // the dataset is not specified, create an 'empty' node
             try
             {
                parEl = AXIOMUtil.stringToOM("<" + lsp.name() + "/>");
             }
             catch (XMLStreamException e)
             {
                throw new RuntimeException(e);
             }
          }
          else if ("DATASET-HANDLE".equalsIgnoreCase(lsp.type()))
          {
             // in this case, the XML looks like this:
             // root element: the DATASET-HANDLE parameter name
             // first child: the schema
             // second child: the dataset
             // we need to move the schema node to the second child and use that XML

             OMElement schemaEl = null;
             OMElement dsEl = null;
             Iterator<OMElement> iter = parEl.getChildElements();
             while (iter.hasNext())
             {
                OMElement child = iter.next();
                if (child.getNamespace() != null &&
                    child.getNamespace().getNamespaceURI().equals("http://www.w3.org/2001/XMLSchema"))
                {
                   schemaEl = child;
                }
                else
                {
                   if (dsEl != null)
                   {
                      dsEl = null;
                      break;
                   }
                   dsEl = child;
                }
             }

             if (schemaEl != null && dsEl != null)
             {
                schemaEl.detach();
                iter = dsEl.getChildElements();
                List<OMElement> rows = new ArrayList<>();
                while (iter.hasNext())
                {
                   OMElement row = iter.next();
                   rows.add(row);
                }
                for (OMElement row : rows)
                {
                   row.detach();
                }

                // schema must be first!
                dsEl.addChild(schemaEl);
                for (OMElement row : rows)
                {
                   dsEl.addChild(row);
                }
                parEl = dsEl;
             }
             else
             {
                // TODO: error ?
             }
          }

          SoapHandler.removeNamespaces(parEl);
          String xmlDs = parEl.toString();

          DatasetWrapper wrapper = new DatasetWrapper(lsp.input(), lsp.output(), false);
          wrapper.setXmlDataset(xmlDs);
          wrapper.setAsXml(true);

          return wrapper;
       }

       /**
        * Raises an error so that the request stops with a 5xx internal server error.
        *
        * @param    bdt
        *           The BDT for which the value could not be parsed.
        * @param    val
        *           The attempted value to assign.
        */
       @Override
       protected void notAValue(BaseDataType bdt, String val)
       throws RequestArgumentError
       {
          if (val == null)
          {
             val = "?";
          }

          String type = bdt.getTypeName().toLowerCase();
          boolean quote = true;
          switch (bdt.getClass().getSimpleName())
          {
             case "integer":
                type = "integer";
                break;
             case "recid":
             case "int64":
                type = "long";
                break;
             case "decimal":
                type = "decimal";
                break;
             case "logical":
                type = "boolean";
                break;
             case "date":
                type = "date";
                break;
             case "datetimetz":
                quote = false;
                if (val.isEmpty())
                {
                   throw new RequestArgumentError("String index out of range: 0 (10914)");
                }
                type = "dateTime";
                break;
             case "datetime":
                if (val.isEmpty())
                {
                   throw new RequestArgumentError("String index out of range: 0 (10914)");
                }
                if (val.equals("?"))
                {
                   val = "";
                }
                type = "dateTime";
                break;
          }

          if (quote)
          {
             val = "''''" + val + "''''";
          }
          throw new RequestArgumentError("Invalid " + type + " value " + val + " sent to deserializer (10914)");
       }

       /**
        * Get the service parameter with the given name.
        *
        * @param    source
        *           The parameter's name.
        *
        * @return   The {@link LegacyServiceParameter} from the {@link #params} map.
        */
       private LegacyServiceParameter getLegacyParameter(String source)
       {
          if (!source.startsWith("SOAP:"))
          {
             throw new RuntimeException("Invalid SOAP source: " + source);
          }

          source = source.substring("SOAP:".length());
          source = source.toLowerCase();

          return params.get(source);
       }

       /**
        * Get the SOAP element from the envelope's body, for the specified parameter.
        *
        * @param    source
        *           The parameter name.
        *
        * @return   The SOAP element for this parameter, from the envelope's body.
        */
       private OMElement getSoapParameter(String source)
       {
          if (!source.startsWith("SOAP:"))
          {
             throw new RuntimeException("Invalid SOAP source: " + source);
          }

          source = source.substring("SOAP:".length());
          // source = source.toLowerCase();

          BaseSchemaType bst = soapParams.get(source);
          String xmlName = bst.xmlName;

          QName parqn = new QName(soapRequest.getQName().getNamespaceURI(), xmlName);
          OMElement parEl = soapRequest.getFirstChildWithName(parqn);

          return parEl == null ? soapRequest.getFirstChildWithName(new QName(xmlName)) : parEl;
       }

       /**
        * Get the SOAP elements from the envelope's body, for the specified parameter.
        *
        * @param    source
        *           The parameter name.
        *
        * @return   The SOAP elements for this parameter, from the envelope's body.
        */
       private Iterator<OMElement> getSoapParameters(String source)
       {
          if (!source.startsWith("SOAP:"))
          {
             throw new RuntimeException("Invalid SOAP source: " + source);
          }

          source = source.substring("SOAP:".length());
          // source = source.toLowerCase();

          QName parqn = new QName(soapRequest.getQName().getNamespaceURI(), source);
          return soapRequest.getChildrenWithName(parqn);
       }

       /**
        * Create a new {@link BaseDataType} instance and restore its value from the given string,
        * based on the XSD type.
        *
        * @param    xsdType
        *           The parameter's XSD type.
        * @param    type
        *           The BDT sub-class.
        * @param    sval
        *           The string representation of this argument.
        *
        * @return   The new {@link BaseDataType} or {@link MemoryBuffer} instance for this argument.
        */
       private Object createArgument(String xsdType, Class<?> type, String sval)
       throws RequestArgumentError
       {
          BaseDataType bdt = BaseDataType.generateUnknown(type);

          if (type == handle.class)
          {
             notAValue(bdt, sval);

             return bdt;
          }

          if (sval == null)
          {
             return bdt;
          }
          switch (xsdType)
          {
             case "xsd:string":
                bdt.assign(DatatypeConverter.parseString(sval));
                break;

             case "xsd:int":
                bdt.assign(new BigInteger(Long.toString(DatatypeConverter.parseLong(sval))));
                break;

             case "xsd:long":
                bdt.assign(new BigInteger(Long.toString(DatatypeConverter.parseLong(sval))));
                break;

             case "xsd:decimal":
                if (sval.isEmpty())
                {
                   notAValue(bdt, "");
                }
                bdt.assign(DatatypeConverter.parseDecimal(sval));
                break;

             case "xsd:dateTime":
             case "xsd:date":
                if (sval.isEmpty())
                {
                   notAValue(bdt, "");
                }
                else if (sval.equals("?"))
                {
                   notAValue(bdt, "?");
                }
                if (bdt.getClass() == date.class)
                {
                   // we need only the date part
                   if (sval.indexOf("T") > 0)
                   {
                      sval = sval.substring(0, sval.indexOf("T"));
                   }
                }
                else if (bdt.getClass() == datetime.class)
                {
                   // TODO: we need only the datetime part
                }

                date d = null;
                try
                {
                   d = date.parseIso8601(sval);

                   if (d == null)
                   {
                      notAValue(bdt, sval);
                   }
                }
                catch (Throwable t)
                {
                   notAValue(bdt, sval);
                }

                if (bdt instanceof datetimetz)
                {
                   if (!(d instanceof datetimetz))
                   {
                      notAValue(bdt, sval);
                   }
                   ((datetimetz) bdt).assign(d);
                }
                else if (bdt instanceof datetime)
                {
                   if (!(d instanceof datetime))
                   {
                      notAValue(bdt, sval);
                   }

                   if (d instanceof datetimetz)
                   {
                      Calendar c = ((datetimetz) d).calendarValue();
                      ((datetime) bdt).assign(new datetime(c.getTime(), c.getTimeZone()));
                   }
                   else
                   {
                      ((datetime) bdt).assign(d);
                   }
                }
                else
                {
                   if (d instanceof datetimetz)
                   {
                      Calendar c = ((datetimetz) d).calendarValue();
                      ((date) bdt).assign(new date(c.getTime(), c.getTimeZone()));
                   }
                   else
                   {
                      ((date) bdt).assign(d);
                   }
                }
                break;

             case "xsd:boolean":
                if (sval.isEmpty())
                {
                   notAValue(bdt, "");
                }

                bdt.assign(ConverterUtil.convertToBoolean(sval));
                break;

             case "xsd:base64Binary":
                byte[] bytes = Base64.safeBase64ToByteArray(sval);
                if (bdt instanceof memptr || bdt instanceof blob)
                {
                   MemoryBuffer mb = new MemoryBuffer(bytes);
                   return mb;
                }

                if (bdt instanceof rowid)
                {
                   if (bytes.length > 0)
                   {
                      long val = bytes[0] & 0xffL;
                      for (int i = 1; i < Math.min(bytes.length, 8); i++)
                      {
                         val += (bytes[i] & 0xffL) << (8 * i);
                      }
                      ((rowid) bdt).assign(new rowid(val));
                   }
                }
                else
                {
                   ((BinaryData) bdt).assign(bytes);
                }
                break;
          }

          return bdt;
       }
    }