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