# JsonResponseArguments.java

    /*
    ** Module   : JsonResponseArguments.java
    ** Abstract : APIs to serialize the OUTPUT arguments in the HTTP servlet response, as JSON.
    **
    ** Copyright (c) 2019-2022, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20190614 First version.
    ** 002 CA  20190628 Added TABLE output support.
    ** 003 CA  20190812 Changed getTableName to getStructureName.
    ** 004 CA  20200427 Fixes for DATASET, TABLE and error serialization.
    **     CA  20200514 Refactoring to allow common code for SOAP services support.
    **     CA  20200528 Added APIs for REST extent arguments (not implemented yet).
    ** 005 CA  20210304 The cookies must follow RFC2965.
    **     CA  20211112 Do not write an empty table.
    **     CA  20220329 Added support for REST services written directly in Java.
    ** 006 AL2 20250325 Override reset abstract method. REST responses are cached, so reset is no-op.
    */

    /*
    ** 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.reflect.*;
    import java.math.*;
    import java.util.*;
    import javax.servlet.http.*;
    import javax.xml.bind.*;
    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.node.*;
    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.util.*;

    /**
     * Helper class to serialize a parameter's value, JSON-style.
     */
    class JsonResponseArguments
    extends ResponseArguments
    {
       /** The root JSON element. */
       private final ObjectNode root;

       /** The JSON 'response' element - here any JSON arguments will be attached. */
       private final ObjectNode jsonResponse;

       /** Flag identifying if any JSON arguments were written. */
       private boolean hasArgs = false;

       /**
        * Initialize this instance by creating the <code>{ "response": {} }</code> JSON.
        */
       public JsonResponseArguments()
       {
          root = JsonNodeFactory.instance.objectNode();
          jsonResponse = root.putObject("response");
       }

       /**
        * Serialize the error, to be included in the response.
        *
        * @param    err
        *           The error.
        *
        * @return   The serialized version of the error.
        */
       public String writeError(LegacyError err)
       {
          // format is: _retVal, _errors: [{_errorMsg, _errorNum}]
          ObjectNode root = JsonNodeFactory.instance.objectNode();
          ArrayNode errRoot = root.putArray("_errors");
          if (err instanceof AppError && ((AppError) err).isFromReturn())
          {
             root.put("_retVal", ((AppError) err).getReturnValue().toStringMessage());

             ObjectNode errNode = errRoot.addObject();
             errNode.put("_errorMsg",
                         "ERROR condition: The Server application has returned an error. (7243) (7211)");
             errNode.put("_errorNum", 0);
          }
          else
          {
             int numErrors = err.getNumMessages().intValue();
             integer idx = new integer();
             for (int i = 1; i <= numErrors; i++)
             {
                idx.assign(i);

                ObjectNode errNode = errRoot.addObject();
                errNode.put("_errorMsg", String.format("ERROR condition: %s (7211)",
                                                       err.getMessage(idx).toStringMessage()));
                errNode.put("_errorNum", err.getMessageNum(idx).toStringMessage());
             }
          }

          return root.toString();
       }

       /**
        * Determine if unknown values must be ignored.
        *
        * @return    Always <code>true</code>
        */
       @Override
       protected boolean ignoreUnknown()
       {
          return true;
       }

       /**
        * Serialize this argument's values, considering the target format.
        *
        * @param    stream
        *           The response stream.
        * @param    target
        *           The argument's encoded target.
        * @param    val
        *           The argument's value.
        * @param    response
        *           The HTTP response.
        */
       protected void writeExtentArgument(OutputStream        stream,
                                          String              target,
                                          Object              val,
                                          HttpServletResponse response)
       throws IOException
       {
          // TODO: fix this
          UnimplementedFeature.unsupported("REST extent arguments");
       }

       /**
        * Serialize this argument's value, considering the target format.
        *
        * @param    stream
        *           The response stream.
        * @param    target
        *           The argument's encoded target.
        * @param    sval
        *           The argument's string-converted value.
        * @param    val
        *           The argument's value.
        * @param    response
        *           The HTTP response.
        */
       @Override
       protected void writeArgumentInt(OutputStream        stream,
                                       String              target,
                                       String              sval,
                                       Object              val,
                                       HttpServletResponse response)
       throws IOException
       {

          if (target.startsWith("${"))
          {
             target = target.substring(2);
          }
          if (target.endsWith("}"))
          {
             target = target.substring(0, target.length() - 1);
          }

          switch (target)
          {
             case "http.body":
                // TODO: what is the correct format here? especially in case of DATASET/TABLE
                // TODO: in .restoe, there is an association with a Java type, for each argument
                //       maybe those are used to build and serialize to string?
                stream.write(sval.getBytes());
                return;

             case "http.statuscode":
                response.setStatus(Integer.parseInt(sval));
                return;
          }

          if (target.startsWith("http.header["))
          {
             String name = RestHandler.extractVal(target);

             // TODO: is 'toString' correct?
             response.setHeader(name, sval);
             return;
          }

          if (target.startsWith("rest.cookieparam["))
          {
             // TODO: is 'toString' correct?
             String name = RestHandler.extractVal(target);
             Cookie cookie = new Cookie(name, sval);
             cookie.setVersion(1);
             response.addCookie(cookie);
             return;
          }

          if (target.startsWith("rest.cookieparam[")) // duplicate case. Was this the intent?
          {
             // TODO: is 'toString' correct?
             String name = RestHandler.extractVal(target);
             Cookie cookie = new Cookie(name, sval);
             cookie.setVersion(1);
             response.addCookie(cookie);
             return;
          }

          if (target.startsWith("json.object['response']."))
          {
             // TODO: unknown values? how are they serialized?

             String jsonMode = target.substring("json.object['response'].".length());
             String attr = RestHandler.extractVal(jsonMode);

             if (val instanceof JsonNode)
             {
                // already created, nothing else to do
                hasArgs = true;
                jsonResponse.set(attr, (JsonNode) val);
                return;
             }

             jsonMode = jsonMode.substring(0, jsonMode.indexOf('['));

             switch (jsonMode.toLowerCase())
             {
                case "boolean":
                   jsonResponse.put(attr, Boolean.valueOf(val.toString()));
                   break;
                case "integervalue":
                   jsonResponse.put(attr, Integer.parseInt(val.toString()));
                   break;
                case "longvalue":
                   jsonResponse.put(attr, Long.parseLong(val.toString()));
                   break;
                case "decimalvalue":
                   jsonResponse.put(attr, new BigDecimal(val.toString()));
                   break;
                case "string":
                   jsonResponse.put(attr, val.toString());
                   break;
                case "object":
                   if (val instanceof ObjectNode) // always false, see the previous [if] statement
                   {
                      jsonResponse.set(attr, (ObjectNode) val);
                   }
                   else
                   {
                      jsonResponse.put(attr, val.toString());
                   }
                   break;
                default:
                   return;
             }

             hasArgs = true;
          }
       }

       /**
        * Flush the arguments to the response stream (i.e. HTTP body).
        *
        * @param    stream
        *           The stream to write the parameters.
        * @param    response
        *           The servlet response.
        */
       @Override
       protected void flushArguments(OutputStream stream, HttpServletResponse response)
       throws IOException
       {
          if (hasArgs)
          {
             stream.write(root.toString().getBytes());
          }
       }

       /**
        * Serialize the specified result-set.
        *
        * @param    target
        *           The target OUTPUT parameter.
        * @param    val
        *           The table result set, as received from the remote side.
        *
        * @return   The string-representation of this table.
        */
       @Override
       protected Object writeTable(String target, TableWrapper val)
       {
          ObjectNode root = JsonNodeFactory.instance.objectNode();
          ArrayNode table = root.putArray(val.getTableName());

          writeTable(table, val.getProperties(), val.getRows());

          return root;
       }

       /**
        * Serialize the specified dataset.
        *
        * @param    target
        *           The target OUTPUT parameter.
        * @param    val
        *           The dataset, as received from the remote side.
        *
        * @return   The serialized version of this dataset.
        */
       @Override
       protected Object writeDataSet(String target, DataSetContainer val)
       {
          ObjectNode root = JsonNodeFactory.instance.objectNode();
          ObjectNode dsRoot = root.putObject(val.getStructureName());

          for (DsTableDefinition dsTable : val.getTableDefinitions())
          {
             if (!dsTable.getRows().isEmpty())
             {
                ArrayNode table = dsRoot.putArray(dsTable.getName());
                writeTable(table, dsTable.getProperties(), dsTable.getRows());
             }
          }

          return root;
       }

       /**
        * Reset the state of the response arguments. For REST requests, arguments
        * are cached, so there is no need to drop the serializer.
        */
       @Override
       protected void reset()
       {
          // no-op
       }

       /**
        * Write the specified table to JSON.
        *
        * @param    table
        *           The JSON structure.
        * @param    props
        *           The table properties.
        * @param    rows
        *           The table data.
        */
       private void writeTable(ArrayNode table, List<PropertyDefinition> props, List<Object[]> rows)
       {
          for (Object[] row : rows)
          {
             ObjectNode jrow = table.addObject();

             for (int i = 0; i < props.size(); i++)
             {
                PropertyDefinition prop = props.get(i);
                if (prop.isExtent())
                {
                   int sz = Array.getLength(row[i]);
                   Class<?> cls = row[i].getClass().getComponentType();
                   ArrayNode jBdtArr = jrow.putArray(prop.getLegacyName());
                   for (int j = 0; j < sz; j++)
                   {
                      BaseDataType bdt = (BaseDataType) Array.get(row[i], i);

                      if (bdt.isUnknown())
                      {
                         jBdtArr.addNull();
                      }
                      else
                      {
                         String val = toString(bdt);
                         switch (cls.getSimpleName())
                         {
                            case "integer":
                               jBdtArr.add(DatatypeConverter.parseInt(val));
                               break;

                            case "int64":
                               jBdtArr.add(DatatypeConverter.parseLong(val));
                               break;

                            case "decimal":
                               jBdtArr.add(DatatypeConverter.parseDecimal(val));
                               break;

                            case "logical":
                               jBdtArr.add(DatatypeConverter.parseBoolean(val));
                               break;

                            default:
                               jBdtArr.add(val);
                               break;
                         }
                      }
                   }
                }
                else
                {
                   BaseDataType bdt = (BaseDataType) row[i];
                   if (bdt.isUnknown())
                   {
                      if (bdt instanceof blob)
                      {
                         jrow.put(prop.getLegacyName(), "");
                      }
                      else
                      {
                         jrow.putNull(prop.getLegacyName());
                      }
                   }
                   else
                   {
                      String val = toString(bdt);

                      switch (bdt.getClass().getSimpleName())
                      {
                         case "integer":
                            jrow.put(prop.getLegacyName(), DatatypeConverter.parseInt(val));
                            break;

                         case "int64":
                            jrow.put(prop.getLegacyName(), DatatypeConverter.parseLong(val));
                            break;

                         case "decimal":
                            jrow.put(prop.getLegacyName(), DatatypeConverter.parseDecimal(val));
                            break;

                         case "logical":
                            jrow.put(prop.getLegacyName(), DatatypeConverter.parseBoolean(val));
                            break;

                         default:
                            jrow.put(prop.getLegacyName(),  val);
                            break;
                      }
                   }
                }
             }
          }
       }
    }