# DsTableDefinition.java

    /*
    ** Module   : DsTableDefinition.java
    ** Abstract : A container for a DataSet Table metadata.
    **
    ** Copyright (c) 2019-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 OM  20130708 Created initial version.
    **     OM  20190716 Updated Javadocs.
    **     OM  20190721 Added new table properties to transport protocol.
    **     CA  20190812 Added toString().
    ** 002 CA  20191119 Added support for before-table.
    ** 003 CA  20200427 BLOB fields must be serialized via MemoryBuffer.
    **     CA  20200514 In some cases, BLOB fields are already MemoryBuffer.
    ** 004 OM  20200906 New ORM implementation.
    ** 005 IAS 20200908 Rework (de)serialization.
    ** 006 IAS 20200922 Get rid of possible NPE on serialization.
    **     OM  20201120 Added implementations for XML-NODE-NAME, ERROR-STRING and SCHEMA-MARSHAL attributes.
    **     CA  20210213 Added 'isSimpleTable'.
    **     OM  20210309 Do not use DmoMeta as key in TableMapper because temp-tables share the same DmoMeta.
    **     SVL 20210614 Added copyRows parameter.
    **     SVL 20210701 Added clearRows.
    **     IAS 20221108 Fixed support for SCHEMA-MARSHAL == NONE. Re-worked (de)serialization.
    **     IAS 20221111 Fixed 'schemaMarshalLevel', 'xmlNodeName', 'nsURI', and 'nsPrefix' support.
    **     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
    ** 007 CA  20230215 Use PayloadSerializer for serialization.  Fixed blob and object field serialization.
    ** 008 OM  20230315 Better debugging support (improved toString() method).
    ** 009 AS  20250416 For this.name use the buffer's parent name, insted of the buffer name.
    */

    /*
    ** 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 java.io.*;
    import java.util.*;
    import java.util.stream.*;

    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.util.*;

    import static com.goldencode.p2j.persist.AbstractTempTable.*;
    import static com.goldencode.util.NativeTypeSerializer.*;

    /**
     * Container for a temp-table. Used to send the temp-table to a remote side, when remote
     * appserver calls are involved.
     */
    public class DsTableDefinition
    implements Externalizable
    {
       /** Flag indicating that table schema was received */
       boolean receivedSchema = false;

       /** The name of this table. */
       private String name;

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

       /** The list of read properties. */
       private List<PropertyDefinition> properties;

       /** The read data. */
       private List<Object[]> rows;

       /** The read row metadata. */
       private List<Object[]> rowsMeta;

       /** The table BEFORE/AFTER type. */
       private int tableType = TempTable.BeforeType.SIMPLE.ordinal();

       /** The peer table. For an AFTER-TABLE, the peer-table is the BEFORE-TABLE. And vice-versa. */
       private DsTableDefinition peerDef = null;

       /** The number of indexes in this table definition. */
       private int numIndexes = 0;

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

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

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

       /** The ERROR-STRING attribute for this table definition. */
       private String errorString = null;

       /** List of indexes' descriptors */
       private List<IndexDefinition> indexDefs = null;

       /**
        * A string with all indexes of this table definition encoded as a {@code String}.
        *
        * See multiIxCols syntax on
        * https://documentation.progress.com/output/ua/OpenEdge_latest/dvjav/Constructor_2.html
        */
       private String indexes = null;

       /**
        * The SCHEMA-MARSHAL level for the serialized temp-table. It is never {@code SM_DEFAULT} when object is to
        * be serialized.
        */
       private int schemaMarshalLevel;

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

       /**
        * Wrap a custom result set in a class known to P2J, so the remote side can deserialize it.
        *
        * @param   resultSet
        *          The result set, which is a subclass of {@link TableResultSet}.
        */
       public DsTableDefinition(TableResultSet resultSet)
       {
          schemaMarshalLevel = SM_DEFAULT;
          this.resultSet = resultSet;
       }

       /**
        * The "serialization" constructor. Takes a {@code BufferImpl} and prepare for serialization.
        * If it belongs to an AFTER-TABLE, the associated BEFORE-TABLE will also be serialized.
        *
        * @param   target
        *          The {@code BufferImpl} to be serialized.
        * @param   copyRows
        *          <code>true</code>> to transfer rows in INPUT direction. <code>false</code>> to transfer only
        *          table definitions.
        */
       public DsTableDefinition(BufferImpl target, boolean copyRows)
       {
          this.name = target.buffer().getParentTable()._name();
          this.tableType = (target.isAfterBuffer()  ? TempTable.BeforeType.AFTER :
                            target.isBeforeBuffer() ? TempTable.BeforeType.BEFORE :
                                                      TempTable.BeforeType.SIMPLE).ordinal();

          Buffer defBuff = (Buffer) target.defaultBufferHandleNative();
          AbstractTempTable parentTable = (AbstractTempTable) target.buffer().getParentTable();

          int sml = parentTable.getSchemaMarshalLevel();
          this.schemaMarshalLevel = (sml == SM_DEFAULT) ? SM_FULL : sml;
          switch (sml)
          {
             case SM_FULL:
             case SM_MIN:
                this.errorString = defBuff.errorString().toStringMessage();
                // no break, fall through
             case SM_DEFAULT:
                // full table schema is serialized with table parameter
                character tmp = parentTable.namespaceURI();
                this.xmlns = tmp.isUnknown() ? "" : tmp.toJavaType();
                this.xmlPrefix = parentTable.namespacePrefix().toJavaType();
                this.xmlNodeName = parentTable.namespacePrefix().toJavaType();
                this.indexes = TableMapper.getLegacyIndexInfo(parentTable);
                this.numIndexes = this.indexes.split("\\.").length;
                this.indexDefs = TableMapper.getLegacyIndexes(parentTable).map(lif -> new IndexDefinition(lif)).
                         collect(Collectors.toList());
                // no break, fall through

             case SM_NONE:
             default:
                // nothing is marshaled. The protocol is the same but null values are sent instead of data.
                // The receiver peer must have all information readily available.
                break;
          }

          if (this.tableType == TempTable.BeforeType.AFTER.ordinal())
          {
             this.peerDef = new DsTableDefinition((BufferImpl) target.beforeBufferNative(), copyRows);
             this.peerDef.peerDef = this;
          }

          this.resultSet = new TempTableResultSet((Temporary) target, true, true, true, copyRows);
       }

       /**
        * Obtain the peer table definition. For an AFTER-TABLE, the peer-table is its BEFORE-TABLE.
        * And vice-versa: for a BEFORE-TABLE, the peer-table is its AFTER-TABLE.
        * <p>
        * Usage of this method is not encouraged. This is because the caller is not aware of what
        * exactly he will receive and the method logic does not have the means to check whether the
        * returned object is exactly what was desired. Specialized method should be used (namely
        * {@code getBeforeDef()} and {@code getAfterDef()}).
        *
        * @return  the peer {@code DsTableDefinition} as explained above.
        */
       public DsTableDefinition getPeerDef()
       {
          return peerDef;
       }

       /**
        * Configures the peer table definition.
        * <p>
        * Usage of this method is not encouraged. This is because the caller is not aware of what
        * exactly he will set and the method logic does not have the means to check whether the
        * returned object is exactly what was desired. Specialized method should be used (namely
        * {@code setBeforeDef()} and {@code setAfterDef()}). Moreover, the method automatically sets
        * this as an AFTER-BUFFER definition.
        *
        * @param   peerDef
        *          The new peer table definition.
        */
       public void setPeerDef(DsTableDefinition peerDef)
       {
          this.peerDef = peerDef;
          this.tableType = TempTable.BeforeType.AFTER.ordinal();
          this.peerDef.tableType = TempTable.BeforeType.BEFORE.ordinal();
       }

       /**
        * Gets the name of this table.
        *
        * @return  the name of this table.
        */
       public String getName()
       {
          return name;
       }

       /**
        * The SCHEMA-MARSHAL level for the serialized temp-table.
        *
        * @return   The {@link #schemaMarshalLevel}.
        */
       public int getSchemaMarshalLevel()
       {
          return schemaMarshalLevel;
       }

       /**
        * Set the SCHEMA-MARSHAL level for the serialized temp-table.
        *
        * @param    schemaMarshalLevel
        *           The {@link #schemaMarshalLevel}.
        */
       public void setSchemaMarshalLevel(int schemaMarshalLevel)
       {
          this.schemaMarshalLevel = schemaMarshalLevel;
       }

       /**
        * Sets the name of this table.
        *
        * @param   name
        *          The (new) name of this table.
        */
       public void setName(String name)
       {
          this.name = name;
       }

       /**
        * Check that table schema was received.
        *
        * @return <code>true</code> if table schema was received.
        */
       public boolean isReceivedSchema()
       {
          return receivedSchema;
       }

       /**
        * Check whether this is a definition of an AFTER-TABLE.
        *
        * @return  {@code true} if this is an AFTER-TABLE definition.
        */
       public boolean isAfterTable()
       {
          return tableType == TempTable.BeforeType.AFTER.ordinal();
       }

       /**
        * Check whether this is a definition of a BEFORE-TABLE.
        *
        * @return  {@code true} if this is an BEFORE-TABLE definition.
        */
       public boolean isBeforeTable()
       {
          return tableType == TempTable.BeforeType.BEFORE.ordinal();
       }

       /**
        * Check whether this is a definition of a SIMPLE temp-table.
        *
        * @return  {@code true} if this is an SIMPLE temp-table definition.
        */
       public boolean isSimpleTable()
       {
          return tableType == TempTable.BeforeType.SIMPLE.ordinal();
       }

       /**
        * Obtain the number of fields in this table.
        * <p>
        * NOTE: the result of this method is valid only after {@code properties} was set.
        *  <p>
        * TODO: test and confirm whether this method returns the number of legacy fields or this
        *       number includes the meta-fields (ROW-STATE, BEFORE-ROWID, etc)
        *
        *
        * @return  the number of fields in this table.
        */
       public int getNumFields()
       {
          return properties.size();
       }

       /**
        * Returns the number of indexes defined for this table.
        *
        * @return  the number of indexes defined for this table.
        */
       public int getNumIndexes()
       {
          return numIndexes;
       }

       /**
        * Obtain the XML namespace for this table definition.
        *
        * @return  the XML namespace for this table definition.
        */
       public String getXmlns()
       {
          return xmlns;
       }

       /**
        * Obtain the XML prefix for this table definition.
        *
        * @return  the XML prefix for this table definition.
        */
       public String getXmlPrefix()
       {
          return xmlPrefix;
       }

       /**
        * Obtain the XML node name for this table definition.
        *
        * @return  the XML node name for this table definition.
        */
       public String getXmlNodeName()
       {
          return xmlNodeName;
       }

       /**
        * Obtain the ERROR-STRING attribute for this table definition.
        *
        * @return  the ERROR-STRING attribute for this table definition.
        */
       public String getErrorString()
       {
          return errorString;
       }

       /**
        * Obtain the set of indexes of this table definition.
        *
        * @return  the set of indexes of this table definition encoded as a single string.
        *
        * @see     #indexes
        */
       public String getIndexes()
       {
          return indexes;
       }

       /**
        * Get List of indexes' descriptors.
        * @return the List of indexes' descriptors.
        */
       public List<IndexDefinition> getIndexDefs()
       {
          return indexDefs;
       }

       /**
        * Sets the XML namespace for this table definition.
        *
        * @param   xmlns
        *          The (new) XML namespace for this table definition.
        */
       public void setXmlns(String xmlns)
       {
          this.xmlns = xmlns;
       }

       /**
        * Sets the XML prefix for this table definition.
        *
        * @param   xmlPrefix
        *          XML prefix for this table definition.
        */
       public void setXmlPrefix(String xmlPrefix)
       {
          this.xmlPrefix = xmlPrefix;
       }

       /**
        * Sets the XML node name for this table definition.
        *
        * @param   xmlNodeName
        *          XML prefix for this table definition.
        */
       public void setXmlNodeName(String xmlNodeName)
       {
          this.xmlNodeName = xmlNodeName;
       }

       /**
        * Sets the ERROR-STRING attribute for this table definition.
        *
        * @param   err
        *          ERROR-STRING attribute for this table definition.
        */
       public void setErrorString(String err)
       {
          this.errorString = err;
       }

       /**
        * Configures the index3es for this table.
        *
        * @param   numIndexes
        *          The number of indexes in this table definition.
        * @param   indexes
        *          A string with all indexes of this table definition encoded as a {@code String}.
        */
       public void setIndexes(int numIndexes, String indexes)
       {
          this.numIndexes = numIndexes;
          this.indexes = indexes;
       }

       /**
        * Provides an iterator over the read {@link #properties}.  This is used to access the data
        * sent by a remote side, via {@link #readExternal}. Must not be called when {@link #resultSet}
        * is set.
        *
        * @return   See above.
        */
       public Iterator<PropertyDefinition> propertyIterator()
       {
          if (resultSet != null)
          {
             throw new IllegalStateException(
                   "A custom result set must not be specified when calling this method!");
          }

          if (properties == null)
          {
             return Collections.EMPTY_LIST.iterator();
          }

          return properties.iterator();
       }

       /**
        * Provides an iterator over the read {@link #rows}.  This is used to access the data sent by a
        * remote side, via {@link #readExternal}. Must not be called when {@link #resultSet} is set.
        *
        * @return   See above.
        */
       public Iterator<Object[]> rowIterator()
       {
          if (resultSet != null)
          {
             throw new IllegalStateException(
                   "A custom result set must not be specified when calling this method!");
          }

          if (rows == null)
          {
             return Collections.EMPTY_LIST.iterator();
          }

          return rows.iterator();
       }

       /**
        * Get the list of read {@link #properties}.  This is used to access the data sent by a remote
        * side, via {@link #readExternal}. Must not be called when {@link #resultSet} is set.
        *
        * @return   See above.
        */
       public List<PropertyDefinition> getProperties()
       {
          if (resultSet != null)
          {
             throw new IllegalStateException(
                   "A custom result set must not be specified when calling this method!");
          }

          return properties != null ? new ArrayList<>(properties) : Collections.emptyList();
       }

       /**
        * Get the list of read {@link #rows}.  This is used to access the data sent by a remote side,
        * via {@link #readExternal}. Must not be called when {@link #resultSet} is set.
        *
        * @return   See above.
        */
       public List<Object[]> getRows()
       {
          if (resultSet != null)
          {
             throw new IllegalStateException(
                   "A custom result set must not be specified when calling this method!");
          }

          return new ArrayList<>(rows);
       }

       /**
        * Get the meta info for each row (the {@link TempRecord} fields).
        *
        * @return   The meta info for each row.
        *
        * @see  TempRecord#toArray()
        */
       public List<Object[]> getRowsMeta()
       {
          if (resultSet != null)
          {
             throw new IllegalStateException(
                   "A custom result set must not be specified when calling this method!");
          }

          return new ArrayList<>(rowsMeta);
       }

       /**
        * Obtain the {@code TableResultSet} of this table.
        *
        * @return  the {@code TableResultSet} of this table.
        */
       public TableResultSet getResultSet()
       {
          return resultSet;
       }

       /**
        * Send the table definition to the specified output destination.
        *
        * @param    out
        *           The output destination to which the table definition will be sent.
        *
        * @throws   IOException
        *           In case of I/O errors.
        */
       public final void writeExternal(ObjectOutput out)
       throws IOException
       {
          boolean writeSchema = schemaMarshalLevel != SM_NONE;
          out.writeByte(schemaMarshalLevel);
          writeString(out, name);
          out.writeByte(tableType);
          if (writeSchema)
          {
             out.writeByte(numIndexes);
             writeString(out,indexes);
             writeList(out, indexDefs);
             writeString(out,xmlns);
             writeString(out,xmlPrefix);
          }

          // write properties
          Set<Integer> blobProps = null;
          Set<Integer> objectProps = null;
          if (resultSet != null)
          {
             int idx = 0;
             while (resultSet.hasMoreProperties())
             {
                PropertyDefinition property = resultSet.nextProperty();
                if (writeSchema)
                {
                   property.setMarshalLevel(schemaMarshalLevel);
                   out.writeByte(1);
                   property.writeExternal(out);
                }

                if (property.getType() == blob.class)
                {
                   if (blobProps == null)
                   {
                      blobProps = new HashSet<>();
                   }
                   blobProps.add(idx);
                }
                else if (object.class.isAssignableFrom(property.getType()))
                {
                   if (objectProps == null)
                   {
                      objectProps = new HashSet<>();
                   }
                   objectProps.add(idx);
                }
                idx = idx + 1;
             }
          }
          if (writeSchema)
          {
             out.writeByte(0);
          }
          writeIntSet(out, blobProps);
          writeIntSet(out, objectProps);

          // write data
          if (resultSet != null)
          {
             while (resultSet.hasMoreRows())
             {
                Object[] row = resultSet.nextRow();
                if (blobProps != null)
                {
                   Object[] row2 = new Object[row.length];
                   System.arraycopy(row, 0, row2, 0, row2.length);
                   for (int idx : blobProps)
                   {
                      if (row2[idx] instanceof blob)
                      {
                         row2[idx] = new MemoryBuffer(((blob) row2[idx]).getByteArray());
                      }
                   }

                   row = row2;
                }
                if (objectProps != null)
                {
                   Object[] row2 = new Object[row.length];
                   System.arraycopy(row, 0, row2, 0, row2.length);
                   for (int idx : objectProps)
                   {
                      if (row2[idx] instanceof object)
                      {
                         row2[idx] = new LegacyObject((object<?>) row2[idx]);
                      }
                   }

                   row = row2;
                }
                PayloadSerializer.writePayload(out, row);

                Object[] rowMeta = resultSet.nextRowMeta();
                PayloadSerializer.writePayload(out, rowMeta);
             }
          }
          // end data with a [null]
          PayloadSerializer.writePayload(out, null);

          // last thing to do:
          if (isAfterTable())
          {
             peerDef.writeExternal(out);
          }
       }

       /**
        * Read the table definition from the specified input source.
        *
        * @param   in
        *          The input source from which the table definition will be read.
        *
        * @throws  IOException
        *          In case of I/O errors.
        * @throws  ClassNotFoundException
        *          If the read {@code type} of a property can't be resolved to a valid class.
        */
       public final void readExternal(ObjectInput in)
       throws IOException,
              ClassNotFoundException
       {
          schemaMarshalLevel = in.readByte();
          receivedSchema = schemaMarshalLevel != SM_NONE;
          name = readString(in);
          tableType = in.readByte();
          if (receivedSchema)
          {
             numIndexes = in.readByte();
             indexes = readString(in);
             indexDefs = readList(in, () -> new IndexDefinition());
             xmlns = readString(in);
             xmlPrefix = readString(in);
             properties = new ArrayList<>();
             while (in.readByte() != 0)
             {
                PropertyDefinition pd = new PropertyDefinition();
                pd.readExternal(in);
                properties.add(pd);

             }
          }

          Set<Integer> blobProps = readIntSet(in);
          Set<Integer> objectProps = readIntSet(in);

          // read data
          rows = new ArrayList<>();
          rowsMeta = new ArrayList<>();
          Object next = PayloadSerializer.readPayload(in);
          while (next != null)
          {
             Object[] row = (Object[]) next;
             rows.add(row);
             if (blobProps != null)
             {
                for (Integer i : blobProps)
                {
                   row[i] = new blob(((MemoryBuffer) row[i]).getValue());
                }
             }
             if (objectProps != null)
             {
                objectProps.stream().forEach(i -> {
                   LegacyObject ref = (LegacyObject) row[i];
                   ObjectVar<? extends _BaseObject_> v = new ObjectVar<>(ref.getType());
                   v.assign(ref.restore());
                   row[i] = v;
                });
             }

             next = PayloadSerializer.readPayload(in);
             rowsMeta.add((Object[]) next);

             // prepare the next object, stop at [null]
             next = PayloadSerializer.readPayload(in);
          }

          // last thing to do:
          if (isAfterTable())
          {
             // read the before-table
             peerDef = new DsTableDefinition();
             peerDef.readExternal(in);
             peerDef.peerDef = this;
          }
       }

       /**
        * Delete row data.
        */
       public void clearRows()
       {
          rows = null;
          rowsMeta = null;

          if (resultSet != null)
          {
             resultSet.clearRows();
          }
       }

       /**
        * Get a string representation of this table.
        *
        * @return    See above.
        */
       @Override
       public String toString()
       {
          StringBuilder sb = new StringBuilder("TABLE[").append(getName()).append("]\n");

          sb.append("PROPERTIES:\n");
          Iterator<PropertyDefinition> iter = (properties == null) ? null : properties.iterator();
          if (iter != null)
          {
             while (iter.hasNext())
             {
                sb.append(iter.next().toString()).append("\n");
             }
             sb.append("\n");
          }
          else
          {
             sb.append("/none/\n");
          }

          sb.append("ROWS:\n");
          if (resultSet == null || rows == null)
          {
             sb.append("/none/\n");
          }
          else
          {
             Iterator<Object[]> rowIter = rows.iterator();
             while (rowIter.hasNext())
             {
                Object[] data = rowIter.next();
                for (Object d : data)
                {
                   String sd = "";
                   if (d instanceof BaseDataType)
                   {
                      sd = ((BaseDataType) d).toStringMessage();
                   }
                   else
                   {
                      sd = d.toString();
                   }
                   sb.append("[").append(sd).append("]; ");
                }
                sb.append("\n");
             }
          }

          if (peerDef != null && isAfterTable())
          {
             sb.append("BEFORE-").append(peerDef.toString());
          }

          return sb.toString();
       }
    }