# TableWrapper.java

    /*
    ** Module   : TableWrapper.java
    ** Abstract : A wrapper for table result sets, used to serialize a table data and send it to a remote side.
    **
    ** Copyright (c) 2013-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 CA  20130620 Created initial version.
    ** 002 SVL 20140611 Fixed NPE in writeExternal when no result set is transferred. Added tableName.
    ** 003 SVL 20140797 Added tableHandle field.
    ** 004 IAS 20160919 Serialization optimization.
    ** 005 OM  20190716 Updated Javadoc.
    **     CA  20190731 Added more table-related state (xmlns, xmlprefix, indexes).
    **     CA  20190812 Added toString().
    ** 006 CA  20191119 Added support for before-table.
    ** 007 CA  20200427 Allow a TABLE to be read from JSON argument, in case of a remote REST call.
    **                  BLOB fields must be serialized via MemoryBuffer.
    **     CA  20200514 In some cases, BLOB fields are already MemoryBuffer.
    ** 008 ECF 20200906 New ORM implementation.
    ** 009 IAS 20200908 Rework (de)serialization.
    **     OM  20201120 Added implementation of XML-NODE-NAME and ERROR-STRING attributes
    **     OM  20210309 Do not use DmoMeta as key in TableMapper because temp-tables might share the same
    **                  DmoMeta instance.
    **     SVL 20210517 Added isValid().
    **     SVL 20210625 Set table name to the result set structure name. That will allow to create a dynamic
    **                  table on the remote side.
    **     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  20220428 Allow null peerDef at setPeerDef.
    **     CA  20220602 Added basic support for transport of object instances over appserver call.
    **     IAS 20221029 Added support for the SCHEMA-MARSHAL attribute.
    **     IAS 20221102 Re-worked (de)serialization.
    **     IAS 20221104 Added structured indexes data.
    **     IAS 20221111 Fixed 'schemaMarshalLevel' support.
    **     SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
    **     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
    ** 010 CA  20230215 Use PayloadSerializer for serialization.
    */

    /*
    ** 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 static com.goldencode.p2j.persist.AbstractTempTable.*;

    import java.io.*;
    import java.lang.reflect.Array;
    import java.util.*;
    import java.util.stream.*;

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

    /**
     * Any {@link TableResultSet} instance will be wrapped in a {@link TableWrapper} instance before
     * sending it to a remote side.  This class is intended to be used only by P2J runtime.
     */
    public class TableWrapper
    implements Externalizable
    {
       /** 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;

       /** A set of meta information for each row, as given by {@link TempRecord#toArray()}. */
       private List<Object[]> rowsMeta;

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

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

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

       /** Table name. */
       private String tableName;

       /** Flag indicating that table schema was received */
       boolean receivedSchema = false;

       /** Table SCHEMA-MARSHAL attribute value encodes as int */
       private int schemaMarshalLevel;

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

       /** 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 TableWrapper 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;

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

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

       /** Flag indicating if the source is a JSON table. */
       private boolean fromJson = false;

       /**
        * For SOAP web services, the TABLE is sent (and received) serialized as XML.  This includes the schema,
        * also, for TABLE-HANDLE.
        */
       private String xmlTable = null;

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

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

       /**
        * 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    tableHandle
        *           Flag indicating the wrapper is for a TABLE-HANDLE.
        */
       public TableWrapper(boolean input, boolean output, boolean tableHandle)
       {
          this.input = input;
          this.output = output;
          this.tableHandle = tableHandle;
          this.fromJson = true;
       }

       /**
        * 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 TableWrapper(TableResultSet resultSet)
       {
          this.resultSet = resultSet;
          this.tableName = resultSet.getStructureName();
       }

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

       /**
        * Set the {@link #xmlTable}.
        *
        * @param    xmlTable
        *           The XML representation of the table.
        */
       public void setXmlTable(String xmlTable)
       {
          this.xmlTable = xmlTable;
       }

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

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

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

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

       /**
        * Configures this object as a {@code TableWrapper} for an AFTER-TABLE.
        * <p>
        * Note: the {@code peerDef} is set-up as a BEFORE wrapper but the link to this object (its
        *       AFTER-TABLE wrapper) is not created (added as a comment).
        *
        * @param   peerDef
        *          The {@code TableWrapper} for existing temp-table that will be the new BEFORE-TABLE.
        */
       public void setPeerDef(TableWrapper peerDef)
       {
          this.peerDef = peerDef;
          if (peerDef != null)
          {
             this.peerDef.tableType = TempTable.BeforeType.BEFORE.ordinal();
             this.tableType = TempTable.BeforeType.AFTER.ordinal();
             // this.peerDef.peerDef = peerDef;
          }
       }

       /**
        * Get the {@link #peerDef}.
        *
        * @return   See above.
        */
       public TableWrapper getPeerDef()
       {
          return peerDef;
       }

       /**
        * Check if this table 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 #resultSet} is
        * set.
        *
        * @return   The {@link #input} flag.
        */
       public boolean isInput()
       {
          if (resultSet != null)
          {
             throw new IllegalStateException(
               "A custom result set must not be specified when calling this method!");
          }

          return input;
       }

       /**
        * Check if this table 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 #resultSet} is
        * set.
        *
        * @return   The {@link #output} flag.
        */
       public boolean isOutput()
       {
          if (resultSet != null)
          {
             throw new IllegalStateException(
               "A custom result set must not be specified when calling this method!");
          }

          return output;
       }

       /**
        * Check if this table 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 #resultSet} is
        * set.
        *
        * @return   The {@link #append} flag.
        */
       public boolean isAppend()
       {
          if (resultSet != null)
          {
             throw new IllegalStateException(
               "A custom result set must not be specified when calling this method!");
          }

          return append;
       }

       /**
        * Returns table name.
        *
        * @return table name.
        */
       public String getTableName()
       {
          return tableName;
       }

       /**
        * Set table name.
        *
        * @param tableName
        *        table name.
        */
       public void setTableName(String tableName)
       {
          this.tableName = tableName;
       }

       /**
        * Get the {@link #numIndexes}.
        *
        * @return   See above.
        */
       public int getNumIndexes()
       {
          return numIndexes;
       }

       /**
        * Set the {@link #numIndexes}.
        *
        * @param    numIndexes
        *           The number of indexes in this table.
        */
       public void setNumIndexes(int numIndexes)
       {
          this.numIndexes = numIndexes;
       }

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

       /**
        * Set the {@link #indexes}.
        *
        * @param    indexes
        *           The String representation of the indexes.
        */
       public void setIndexes(String indexes)
       {
          this.indexes = indexes;
       }

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

       /**
        * Get the {@link #xmlns XML namespace}.
        *
        * @return   See above.
        */
       public String getXmlns()
       {
          return xmlns;
       }

       /**
        * Set the {@link #xmlns XML namespace}
        *
        * @param    xmlns
        *           The XML namespace.
        */
       public void setXmlns(String xmlns)
       {
          this.xmlns = xmlns;
       }

       /**
        * Get the {@link #xmlPrefix XML prefix}.
        *
        * @return   See above.
        */
       public String getXmlPrefix()
       {
          return xmlPrefix;
       }

       /**
        * Set the {@link #xmlPrefix XML prefix}
        *
        * @param    xmlPrefix
        *           The XML prefix.
        */
       public void setXmlPrefix(String xmlPrefix)
       {
          this.xmlPrefix = xmlPrefix;
       }

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

       /**
        * 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;
       }

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

       /**
        * 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;
       }

       /**
        * 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!");
          }

          return (properties == null ? Collections.EMPTY_LIST : 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!");
          }

          return (rows == null ? Collections.EMPTY_LIST : rows).iterator();
       }

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

          return (rowsMeta == null ? Collections.EMPTY_LIST : rowsMeta).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 ArrayList<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) : new ArrayList<>(0);
       }

       /**
        * 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 list of read {@link #rowsMeta}.  This is used to access the row meta information
        * sent by a remote side, via {@link #readExternal}. Must not be called when {@link #resultSet}
        * is set.
        *
        * @return   See above.
        */
       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);
       }

       /**
        * Set the table's properties.
        *
        * @param    properties
        *           The property definitions.
        */
       public void setProperties(List<PropertyDefinition> properties)
       {
          this.properties = properties;
       }

       /**
        * Set the table's rows.
        *
        * @param    rows
        *           The table data.
        */
       public void setRows(List<Object[]> rows)
       {
          this.rows = rows;
       }

       /**
        * Set the table rows' meta information.
        *
        * @param    rowsMeta
        *           The meta information for each row.
        */
       public void setRowsMeta(List<Object[]> rowsMeta)
       {
          this.rowsMeta = rowsMeta;
       }

       /**
        * 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(fromJson ? 1 : 0);
          out.writeByte(asXml ? 1 : 0);
          out.writeByte(tableHandle ? 1 : 0);

          // write modes
          if (resultSet != null)
          {
             out.writeByte(resultSet.isInput() ? 1 : 0);
             out.writeByte(resultSet.isOutput() ? 1 : 0);
             out.writeByte(resultSet.isAppend() ? 1 : 0);
          }
          else
          {
             out.writeByte(input ? 1 : 0);
             out.writeByte(output ? 1 : 0);
             out.writeByte(append ? 1 : 0);
          }

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

             writeString(out, xmlTable);
             return;
          }

          boolean writeSchema = schemaMarshalLevel != SM_NONE;
          out.writeByte(schemaMarshalLevel);
          if (writeSchema)
          {
             writeString(out, tableName);
             out.writeByte(tableType);
             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);
             }
          }
          PayloadSerializer.writePayload(out, null);

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

       /**
        * 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 the class of property could not be found/loaded.
        */
       @Override
       public final void readExternal(ObjectInput in)
       throws IOException,
              ClassNotFoundException
       {
          fromJson = in.readByte() != 0;
          asXml = in.readByte() != 0;
          tableHandle = in.readByte() != 0;

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

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

          schemaMarshalLevel = in.readByte();
          receivedSchema =  schemaMarshalLevel != SM_NONE;
          if (receivedSchema)
          {
             tableName = readString(in);
             tableType = in.readByte();
             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<>();
          do
          {
             Object next = PayloadSerializer.readPayload(in);
             if (next == null)
             {
                break;
             }

             Object[] row = (Object[]) next;
             rows.add(row);
             if (blobProps != null)
             {
                blobProps.stream().forEach(i -> 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);
          }
          while (true);

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

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

       /**
        * Determines if this wrapper wraps data from a TABLE-HANDLE parameter.
        *
        * @return <code>true</code> if this wrapper wraps data from a TABLE-HANDLE parameter.
        */
       public boolean isTableHandle()
       {
          return tableHandle;
       }

       /**
        * 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 TABLE-HANDLE parameter.
        *
        * @param tableHandle
        *        <code>true</code> if this wrapper wraps data from a TABLE-HANDLE parameter.
        */
       public void setTableHandle(boolean tableHandle)
       {
          this.tableHandle = tableHandle;
       }

       /** Get encoded table SCHEMA-MARSHAL attribute value.
        *
        * @return encoded table SCHEMA-MARSHAL attribute value.
        */
       public int getSchemaMarshalLevel()
       {
          return schemaMarshalLevel;
       }

       /** Set encoded table SCHEMA-MARSHAL attribute value.
        *
        * @param schemaMarshalLevel
        *        encoded table SCHEMA-MARSHAL attribute value.
        */
       public void setSchemaMarshalLevel(int schemaMarshalLevel)
       {
          this.schemaMarshalLevel = schemaMarshalLevel;
       }

       /**
        * Initialize this wrapper with the info from the given temp-table.
        *
        * @param    table
        *           The table to copy state from.
        */
       public void init(TempTable table)
       {
          this.tableName = ((AbstractTempTable) table)._name();
          Buffer defBuff = (Buffer) table.defaultBufferHandleNative();
          this.indexes = TableMapper.getLegacyIndexInfo(table);
          this.numIndexes = indexes.split("\\.").length; // TODO: "." or "\\." ?
          this.indexDefs = TableMapper.getLegacyIndexes(table).map(lif -> new IndexDefinition(lif)).
                                                               collect(Collectors.toList());
          this.xmlns = defBuff.namespaceURI().toStringMessage();
          this.xmlPrefix = defBuff.namespacePrefix().toStringMessage();
       }

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

          s.append("PROPERTIES:\n");
          Iterator<PropertyDefinition> iter = propertyIterator();
          while (iter.hasNext())
          {
             s.append(iter.next().toString()).append("\n");
          }

          s.append("\nROWS:\n");
          Iterator<Object[]> rowIter = rowIterator();
          while (rowIter.hasNext())
          {
             Object[] data = rowIter.next();
             for (Object d : data)
             {
                s.append("[");
                if (d instanceof BaseDataType)
                {
                   s.append(((BaseDataType) d).toStringMessage());
                }
                else if (d.getClass().isArray())
                {
                   int size = Array.getLength(d);
                   s.append("{");
                   for (int f = 0; f < size; f++)
                   {
                      s.append(f).append(":[").append(Array.get(d, f).toString()).append("]; ");
                   }
                   s.append("}");
                }
                else
                {
                   s.append(d.toString());
                }
                s.append("]; ");
             }
             s.append("\n");
          }

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

          return s.toString();
       }

       /**
        * Returns <code>true</code> if this wrapper contains a valid data set.
        *
        * @return <code>true</code> if this wrapper contains a valid data set.
        */
       public boolean isValid()
       {
          return resultSet != null || rows != null;
       }

       /**
        * Explicitly set the result set for this wrapper.
        *
        * @param    resultSet
        *           The result set to be sent to the remote side.
        */
       public void setResultSet(TableResultSet resultSet)
       {
          this.resultSet = resultSet;
       }
    }