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