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