# DatasetWrapper.java

    /*
    ** Module   : DatasetWrapper.java
    ** Abstract : A wrapper for a Dataset, including all component tables, used to serialize the table
    **            data and send it to a remote side.
    **
    ** Copyright (c) 2019-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 OM  20190708 Created initial version based on the TableWrapper.
    **     OM  20190716 Updated Javadoc. Fixed serialization.
    **     CA  20190812 Added toString().
    ** 002 CA  20191119 Added support for before-table.
    ** 003 CA  20200427 Allow a DATASET to be read from JSON argument, in case of a remote REST call.
    ** 004 IAS 20200908 Rework (de)serialization.
    **     CA  20210304 Added getResource() API.
    **     CA  20211112 Allow the DATASET and TABLE parameters for remote SOAP calls to be transferred via XML
    **                  (so that relations, schema, etc is done on the server-side).
    **     CA  20221102 Added a static serialization ID - required when enabling AOP method tracing (see
    **                  p2j.aspects.ltw.MethodTraceAspect).
    **     IAS 20221108 Fixed support for SCHEMA-MARSHAL == NONE.
    **     IAS 20221111 Added 'xmlNodeName', 'nsURI', and 'nsPrefix' fields and fixed
    **                  'schemaMarshalLevel' support.
    ** 005 OM  20231027 Added support for ERROR attribute.
    ** 006 CA  20240426 Track if this container holds a valid dataset.
    */

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

    import static com.goldencode.util.NativeTypeSerializer.*;

    import java.io.*;
    import java.util.*;

    import com.goldencode.p2j.aspects.ltw.MethodTraceAspect;

    /**
     * This wrapper groups multiple {@link DsTableDefinition} and {@link DsRelationDefinition} the
     * components of the {@link DataSet} instance before sending them to a remote side.
     * <p>
     * This class is intended to be used only by FWD runtime.
     */
    public class DatasetWrapper
    implements Externalizable
    {
       /** A static serialization ID - required when enabling AOP method tracing (see {@link MethodTraceAspect}.*/
       private static final long serialVersionUID = 1234567L;

       /** The result set to be sent to the remote side. */
       private DataSetContainer resource = null;

       /** Flag indicating this dataset is sent in INPUT or INPUT-OUTPUT mode. */
       private boolean input;

       /** Flag indicating this dataset is sent in OUTPUT or INPUT-OUTPUT mode. */
       private boolean output;

       /** Flag indicating this dataset is sent in APPEND mode. */
       private boolean append;

       /** The tables definitions to be read for the dataset. */
       private List<DsTableDefinition> tableDefs = null;

       /** The relations definitions to be read for the dataset. */
       private List<DsRelationDefinition> relationsDefs = null;

       /**
        * Determines if this wrapper wraps data from a DATASET-HANDLE parameter. Necessary because of
        * quirks in error handling: calling side needs to know if it is a DATASET-HANDLE parameter
        * (not a DATASET parameter) on the called side.
        */
       private boolean dsHandle = false;

       /** The name of the DataSet. */
       private String dsName = null;

       /** The XML namespace for this dataset. */
       private String xmlns = null;

       /** The XML prefix for this dataset. */
       private String xmlPrefix = null;

       /** The {@code ERROR} attribute of the dataset. */
       private boolean hasError = false;

       /** The XML-NODE-NAME attribute for this dataset. */
       private String xmlNodeName = null;

       /** Flag indicating if the source is a JSON dataset. */
       private boolean fromJson;

       /**
        * For SOAP web services, the DATASET is sent (and received) serialized as XML, as the requester can't
        * (de)serialize it properly (the relations have dependencies on executing a query to determine the
        * related child-table rows).  This includes the schema, also, for DATASET-HANDLE.
        */
       private String xmlDataset = null;

       /** Flag indicating if the transfer is made via XML. */
       private boolean asXml = false;

       /** Flag indicating if the before-table must be interpreted, too. */
       private boolean useBeforeImage = false;

       /** Flag indicating if this container holds a valid dataset. */
       private boolean valid = false;

       /**
        * Default c'tor, explicitly added to allow instances of this class to be created on
        * deserialization. Not for public use.
        */
       public DatasetWrapper()
       {
       }

       /**
        * Create a new wrapper.
        *
        * @param    input
        *           Flag indicating the wrapper is in INPUT mode.
        * @param    output
        *           Flag indicating the wrapper is in OUTPUT mode.
        * @param    dsHandle
        *           Flag indicating the wrapper is for a DATASET-HANDLE.
        */
       public DatasetWrapper(boolean input, boolean output, boolean dsHandle)
       {
          this.input = input;
          this.output = output;
          this.dsHandle = dsHandle;
          this.fromJson = true;
       }

       /**
        * Wrap a custom result set in a class known to FWD, so the remote side can deserialize it.
        *
        * @param    resource
        *           The resource, which is a (subclass of) {@link DataSet}.
        */
       public DatasetWrapper(DataSetContainer resource)
       {
          this.valid = resource != null;
          this.resource = resource;
          this.xmlns = resource.namespaceURI();
          this.xmlPrefix = resource.namespacePrefix();
          this.xmlNodeName = resource.getXmlNodeName();
          this.hasError = resource.hasError();
       }

       /**
        * Wrap a custom result set in a class known to FWD, so the remote side can deserialize it.
        *
        * @param    fromJson
        *           Flag indicating if the dataset is from a JSON source.
        * @param    resource
        *           The resource, which is a (subclass of) {@link DataSet}.
        */
       public DatasetWrapper(boolean fromJson, DataSetContainer resource)
       {
          this(resource);
          this.fromJson = fromJson;
       }

       /**
        * Get the {@link #valid} flag.
        *
        * @return   {@code true} if this container is for a valid dataset.
        */
       public boolean isValid()
       {
          return valid;
       }

       /**
        * Set the {@link #useBeforeImage} flag.
        *
        * @param    useBeforeImage
        *           Flag indicating that the before-image for the dataset is used.
        */
       public void setUseBeforeImage(boolean useBeforeImage)
       {
          this.useBeforeImage = useBeforeImage;
       }

       /**
        * Get the {@link #useBeforeImage} flag.
        *
        * @return   See above.
        */
       public boolean isUseBeforeImage()
       {
          return useBeforeImage;
       }

       /**
        * Set the {@link #xmlDataset}.
        *
        * @param    xmlDataset
        *           The XML representation of the dataset.
        */
       public void setXmlDataset(String xmlDataset)
       {
          this.xmlDataset = xmlDataset;
       }

       /**
        * Get the {@link #xmlDataset}.
        *
        * @return   See above.
        */
       public String getXmlDataset()
       {
          return xmlDataset;
       }

       /**
        * Get the {@link #asXml} flag.
        *
        * @return   See above.
        */
       public boolean isAsXml()
       {
          return asXml;
       }

       /**
        * Set the {@link #asXml} flag.
        *
        * @param   asXml
        *          When <code>true</code>, the dataset is transferred via its XML representation.
        */
       public void setAsXml(boolean asXml)
       {
          this.asXml = asXml;
       }

       /**
        * Get the dataset container resource.
        *
        * @return   The {@link #resource}.
        */
       public DataSetContainer getResource()
       {
          return resource;
       }

       /**
        * Get the XML namespace of the DATASET.
        * @return XML namespace of the DATASET
        */
       public String namespaceURI()
       {
          return xmlns;
       }

       /**
        * Get the XML prefix of the DATASET.
        * @return XML prefix of the DATASET
        */
       public String namespacePrefix()
       {
          return xmlPrefix;
       }

       /**
        * Get the XML node name of the DATASET.
        *
        * @return XML node name of the DATASET
        */
       public String getXmlNodeName()
       {
          return xmlNodeName;
       }

       /**
        * Get the ERROR attribute of the DATASET.
        *
        * @return the ERROR attribute.
        */
       public boolean hasError()
       {
          return hasError;
       }

       /**
        * Returns the list of table definitions for the temp-tables from this {@code DataSet}.
        *
        * @return  the list of table definitions from this {@code DataSet}.
        */
       public List<DsTableDefinition> getTableDefs()
       {
          return tableDefs;
       }

       /**
        * Returns the list of relations definitions between the temp-tables from this {@code DataSet}.
        * All relations are returned, whether they are active or not.
        *
        * @return  the list of relations definitions from this {@code DataSet}.
        */
       public List<DsRelationDefinition> getRelationsDefs()
       {
          return relationsDefs;
       }

       /**
        * Check if this dataset is in INPUT or INPUT-OUTPUT mode. This is used to access the data sent
        * by a remote side, via {@link #readExternal}. Must not be called when {@link #resource} is
        * set.
        *
        * @return   The {@link #input} flag.
        */
       public boolean isInput()
       {
          if (resource != null)
          {
             throw new IllegalStateException(
                   "A custom result set must not be specified when calling this method!");
          }

          return input;
       }

       /**
        * Check if this dataset is in OUTPUT or INPUT-OUTPUT mode. This is used to access the data
        * sent by a remote side, via {@link #readExternal}. Must not be called when {@link #resource}
        * is set.
        *
        * @return   The {@link #output} flag.
        */
       public boolean isOutput()
       {
          if (resource != null)
          {
             throw new IllegalStateException(
                   "A custom result set must not be specified when calling this method!");
          }

          return output;
       }

       /**
        * Check if this dataset is in APPEND mode. This is used to access the data sent by a remote
        * side, via {@link #readExternal}. Must not be called when {@link #resource} is set.
        *
        * @return   The {@link #append} flag.
        */
       public boolean isAppend()
       {
          if (resource != null)
          {
             throw new IllegalStateException(
                   "A custom result set must not be specified when calling this method!");
          }

          return append;
       }

       /**
        * Determines if this wrapper wraps data from a DATASET-HANDLE parameter.
        *
        * @return  {@code true} if this wrapper wraps data from a DATASET-HANDLE parameter.
        */
       public boolean isDatasetHandle()
       {
          return dsHandle;
       }

       /**
        * Check if the source is a JSON.
        *
        * @return   The {@link #fromJson} flag.
        */
       public boolean isFromJson()
       {
          return fromJson;
       }

       /**
        * Set if this wrapper wraps data from a DATASET-HANDLE parameter.
        *
        * @param   datasetHandle
        *          {@code true} if this wrapper wraps data from a DATASET-HANDLE parameter.
        */
       public void setDatasetHandle(boolean datasetHandle)
       {
          this.dsHandle = datasetHandle;
       }

       /**
        * Obtain the dataset name.
        *
        * @return  the dataset name.
        */
       public String getDatasetName()
       {
          return dsName;
       }

       /**
        * Sets the dataset name.
        *
        * @param   dsName
        *          The (new) dataset name.
        */
       public void setDatasetName(String dsName)
       {
          this.dsName = dsName;
       }

       /**
        * Send the result set (metadata and rows) to the specified output destination.
        *
        * @param    out
        *           The output destination to which the result set will be sent.
        *
        * @throws   IOException
        *           In case of I/O errors.
        */
       public final void writeExternal(ObjectOutput out)
       throws IOException
       {
          out.writeByte(resource != null ? 1 : 0);
          out.writeByte(fromJson ? 1 : 0);
          out.writeByte(asXml ? 1 : 0);
          out.writeByte(dsHandle ? 1 : 0);
          out.writeByte(useBeforeImage ? 1 : 0);
          out.writeByte(hasError ? 1 : 0);

          // write modes
          if (resource != null)
          {
             out.writeByte(resource.isInput() ? 1 : 0);
             out.writeByte(resource.isOutput() ? 1 : 0);
             out.writeByte(resource.isAppend() ? 1 : 0);

             if (asXml)
             {
                // nothing else is sent,

                writeString(out, xmlDataset);
                return;
             }

             writeString(out, resource.getStructureName());
          }
          else
          {
             out.writeByte(input ? 1 : 0);
             out.writeByte(output ? 1 : 0);
             out.writeByte(append ? 1 : 0);

             if (asXml)
             {
                // nothing else is sent,

                writeString(out, xmlDataset);
                return;
             }

             writeString(out, dsName);
          }
          writeString(out, xmlns);
          writeString(out, xmlPrefix);
          writeString(out, xmlNodeName);

          // write properties
          if (resource != null)
          {
             out.writeInt(resource.getTableCount());
             if (resource.getTableCount() > 0)
             {
                for (DsTableDefinition table : resource.getTableDefinitions())
                {
                   table.writeExternal(out);
                }
             }
          }
          else
          {
             out.writeInt(0);
          }

          if (resource != null)
          {
             out.writeInt(resource.getRelationCount());
             if (resource.getRelationCount() > 0)
             {
                for (DsRelationDefinition rel : resource.getRelationDefinitions())
                {
                   rel.writeExternal(out);
                }
             }
          }
          else
          {
             out.writeInt(0);
          }
       }

       /**
        * Read the result set (metadata and rows) from the specified input source.
        *
        * @param   in
        *          The input source from which the result set will be read.
        *
        * @throws  IOException
        *          In case of I/O errors.
        * @throws  ClassNotFoundException
        *          If a class load operation fails.
        */
       public final void readExternal(ObjectInput in)
       throws IOException,
              ClassNotFoundException
       {
          valid = in.readByte() != 0;
          fromJson = in.readByte() != 0;
          asXml = in.readByte() != 0;
          dsHandle = in.readByte() != 0;
          useBeforeImage = in.readByte() != 0;
          hasError = in.readByte() != 0;

          // read modes
          input = in.readByte() != 0;
          output = in.readByte() != 0;
          append = in.readByte() != 0;

          if (asXml)
          {
             // nothing else is sent
             xmlDataset = readString(in);
             return;
          }

          dsName = readString(in);
          xmlns = readString(in);
          xmlPrefix = readString(in);
          xmlNodeName = readString(in);

          int n = in.readInt();
          if (n != 0)
          {
             tableDefs = new ArrayList<>(n);
             for (int k = 0; k < n; k++)
             {
                DsTableDefinition tDef = new DsTableDefinition();
                tDef.readExternal(in);
                tableDefs.add(tDef); // it will automatically read the before-table, too
             }
          }

          n = in.readInt();
          if (n != 0)
          {
             relationsDefs = new ArrayList<>(n);
             for (int k = 0; k < n; k++)
             {
                DsRelationDefinition tDef = new DsRelationDefinition();
                tDef.readExternal(in);
                relationsDefs.add(tDef);
             }
          }
       }

       /**
        * Obtain a {@code TableWrapper} for a specific table of this {@code DataSet}. The table is
        * identified by its index in the buffer list.
        *
        * @param   k
        *          The index of the table whose {@code TableWrapper} will be returned.
        *
        * @return  the table wrapper for the target table.
        */
       public TableWrapper getTableWrapper(int k)
       {
          DsTableDefinition tdef = tableDefs.get(k);

          TempTableResultSet res = this.output ? new TempTableResultSet(this.output, this.append)
                                               : new TempTableResultSet();
          res.setProperties(tdef.getProperties());
          res.setRows(tdef.getRows());
          res.setRowsMeta(tdef.getRowsMeta());
          res.setStructureName(tdef.getName());

          DsTableDefinition btdef = tdef.getPeerDef();
          TempTableResultSet bres = null;
          if (btdef != null)
          {
             bres = this.output ? new TempTableResultSet(this.output, this.append)
                                : new TempTableResultSet();
             bres.setProperties(btdef.getProperties());
             bres.setRows(btdef.getRows());
             bres.setRowsMeta(btdef.getRowsMeta());
             bres.setStructureName(btdef.getName());
          }

          try
          {
             TableWrapper tw = new TableWrapper(fromJson, res);
             tw.setTableName(tdef.getName());
             tw.setSchemaMarshalLevel(tdef.getSchemaMarshalLevel());
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos);
             tw.writeExternal(oos);
             oos.close();
             tw = new TableWrapper();
             tw.readExternal(new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())));
             tw.receivedSchema = tdef.receivedSchema;
             if (bres != null)
             {
                TableWrapper btw = new TableWrapper(bres);
                baos = new ByteArrayOutputStream();
                oos = new ObjectOutputStream(baos);
                btw.writeExternal(oos);
                oos.close();
                btw = new TableWrapper();
                btw.readExternal(new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())));
                tw.setPeerDef(btw);
             }

             return tw;
          }
          catch (Exception e)
          {
             throw new RuntimeException(e);
          }
       }

       /**
        * Get a string representation of this dataset.
        *
        * @return   See above.
        */
       @Override
       public String toString()
       {
          StringBuilder s = new StringBuilder("DATASET[" + (dsName == null ? resource == null ? "null"
                : resource.structureName
                : dsName) + "]");
          s.append("\n");

          if (tableDefs != null)
          {
             for (DsTableDefinition dst : tableDefs)
             {
                s.append(dst.toString());
                s.append("\n");
             }
          }
          else if (resource != null)
          {
             for (DsTableDefinition dst : resource.getTableDefinitions())
             {
                s.append(dst.toString());
                s.append("\n");
             }
          }

          if (relationsDefs != null)
          {
             for (DsRelationDefinition dsr : relationsDefs)
             {
                s.append(dsr.toString());
                s.append("\n");
             }
          }
          else if (resource != null)
          {
             for (DsRelationDefinition dsr : resource.getRelationDefinitions())
             {
                s.append(dsr.toString());
                s.append("\n");
             }
          }

          return s.toString();
       }
    }