# RecordBufferSerializer.java

    /*
    ** Module   : RecordBufferSerializer.java
    ** Abstract : Handles serialization of RecordBuffer objects.
    **
    ** Copyright (c) 2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 AP  20250129 Created initial version.
     */

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

    import java.sql.*;
    import java.time.*;
    import java.util.*;

    /**
     * {@link RecordBufferSerializer} objects are used for retrieving the data from the SQL table(s) associated to
     * a {@link RecordBuffer}. Method {@link RecordBufferSerializer#sqlTableContent} returns the data as a Map,
     * that can be further serialized.
     */
    public class RecordBufferSerializer
    {
       /** {@link Session} associated with the serialized {@link RecordBuffer} */
       private final Session session;

       /** {@link DmoMeta} associated with the serialized {@link RecordBuffer} */
       private final DmoMeta dmoInfo;

       /** List of normalized extent fields */
       private final List<Property> normalizedFields;

       /** Mapping from lowercased SQL column names from {@link Property#column} to the actual {@link Property} */
       private final Map<String, Property> sqlNameNormalizedFields;

       /** Flag to indicate whether the legacy column names should be used or the SQL ones */
       private final boolean useLegacyFieldsName;

       /** Map that represents the current row that is being populated. The keys are the column's names and the
        * values represent either Strings when the column holds scalars or List<String> when it holds extents */
       private Map<String, Object> currentRowObject;

       /**
        * Constructor for {@link RecordBufferSerializer}.
        *
        * @param recordBuffer
        *        The {@link RecordBuffer} for which the associated SQL table needs to be serialized
        * @param useLegacyFieldsName
        *        Flag used to set whether the SQL field names should be used or the legacy ones
        */
       public RecordBufferSerializer(RecordBuffer recordBuffer, boolean useLegacyFieldsName)
       {
          this.dmoInfo = recordBuffer.getDmoInfo();
          this.session = recordBuffer.getPersistence().getSession(!dmoInfo.multiTenant);
          this.normalizedFields = new ArrayList<>();
          this.useLegacyFieldsName = useLegacyFieldsName;
          this.sqlNameNormalizedFields = new HashMap<>();
       }

       /** Constructor for {@link RecordBufferSerializer} with the
        * {@link RecordBufferSerializer#useLegacyFieldsName} set to true by default.
        *
        * @param recordBuffer
        *        The {@link RecordBuffer} for which the associated SQL table needs to be serialized
        */
       public RecordBufferSerializer(RecordBuffer recordBuffer)
       {
          this(recordBuffer, true);
       }

       /**
        * Get the content of the SQL table associated with this buffer as a List of Maps. A Map represents a
        * record. The key is the field name. The value is either a scalar or a List of strings representing an
        * extent.
        *
        * @return List of Maps, where the key is the field name and the value is a String representing a scalar
        *         value or a List of Strings representing the extent values
        *
        * @throws PersistenceException
        *         If the JDBC connection has been closed or there was an error checking its status.
        * @throws SQLException
        *         If a database access error occurs, or if any of the SQL operations (preparation, execution, or
        *         result handling) fail
        */
       public List<Map<String, Object>> sqlTableContent()
       throws PersistenceException, SQLException
       {
          Connection conn = this.session.getConnection();
          List<Map<String, Object>> results = new ArrayList<>();
          try (PreparedStatement ps = conn.prepareStatement("select * from " + dmoInfo.sqlTable);
               ResultSet res = ps.executeQuery())
          {
             while (res.next())
             {
                currentRowObject = new HashMap<>();
                results.add(currentRowObject);
                handleSqlRow(res);
                addNormalizedExtentFields(conn, res.getInt(1));
             }
          }
          return results;
       }

       /**
        * Iterates over all the fields from the associated DMO and populates the
        * {@link RecordBufferSerializer#currentRowObject} with data ONLY from the main table. It doesn't handle
        * the tables where the extent values are saved. In this method ONLY the scalar values and the denormalized
        * fields are handled.
        *
        * @param resultSet
        *        The current {@code ResultSet}
        *
        * @throws SQLException
        *         If a database access error occurs, or if any of the SQL operations (preparation, execution, or
        *         result handling) fail
        */
       @SuppressWarnings("unchecked")
       private void handleSqlRow(ResultSet resultSet)
       throws SQLException
       {
          Iterator<Property> fieldsIterator = dmoInfo.getFields(false);

          while (fieldsIterator.hasNext())
          {
             Property field = fieldsIterator.next();
             String label = getFieldName(field);

             // this is a scalar value
             if (field.extent == 0)
             {
                String value = getFullValueForField(resultSet, field);
                currentRowObject.put(label, value);
             }
             // this is an extent, but denormalized, so the data is in the same table
             else if (field.denormalized)
             {
                String value = getFullValueForField(resultSet, field);
                List<String> currentValuesForExtent = (List<String>) currentRowObject.get(label);

                if (currentValuesForExtent == null)
                {
                   currentValuesForExtent = new ArrayList<>();
                   currentRowObject.put(label, currentValuesForExtent);
                }
                currentValuesForExtent.add(value);
             }
             // this is normalized extent, and on the first row in the resultSet
             else if (resultSet.isFirst())
             {
                // 'cache' the normalized fields for further usage
                sqlNameNormalizedFields.put(field.column.toLowerCase(), field);
                normalizedFields.add(field);
             }
          }
       }

       /**
        * Gets the full value for a field in the {@link ResultSet}, handling the case where the field is of type
        * datetime-tz.
        *
        * @param resultSet
        *        The current {@code ResultSet}
        * @param field
        *        The {@link Property} for which to retrieve the value
        *
        * @return Full value for a field in the {@link ResultSet} or null if SQL value is NULL
        *
        * @throws SQLException
        *         If the columnLabel is not valid; if a database access error occurs or this method is called on a
        *         closed result set
        */
       private String getFullValueForField(ResultSet resultSet, Property field)
       throws SQLException
       {
          if (resultSet.getString(field.column) == null)
          {
             return null;
          }

          if (!field._isDatetimeTz)
          {
             return resultSet.getString(field.column);
          }

          Timestamp tt = resultSet.getTimestamp(field.column);
          if (tt == null)
          {
             return null;
          }
          else
          {
             int offset = resultSet.getInt(field.column + DDLGeneratorWorker.DTZ_OFFSET); // DTZ_OFFSET
             ZoneOffset zoneOffset = ZoneOffset.ofHoursMinutes(offset / 60, offset % 60);
             OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(tt.toInstant(), zoneOffset);
             return offsetDateTime.toString();
          }
       }

       /**
        * Adds the normalized extent fields to {@link RecordBufferSerializer#currentRowObject}. It first adds the
        * field names as key in the {@link RecordBufferSerializer#currentRowObject} map. Then, iterates through
        * all the sqls used to load the table data and populates the lists in the map using
        * {@link RecordBufferSerializer#handleSqlNormalizedRow(ResultSet)}.
        *
        * @param conn
        *        The {@link Connection} object used to interact with the database
        * @param rowId
        *        The ID of the row for which to fetch and process data
        *
        * @throws SQLException
        *         If a database access error occurs, or if any of the SQL operations (preparation, execution, or
        *         result handling) fail
        */
       private void addNormalizedExtentFields(Connection conn, int rowId)
       throws SQLException
       {
          for (Property normalizedField : normalizedFields)
          {
             currentRowObject.put(getFieldName(normalizedField), new ArrayList<>());
          }

          String[] sqls = dmoInfo.getRecordMeta().getLoadSql();
          for (int i = 1; i < sqls.length; i++)
          {
             try (PreparedStatement ps = conn.prepareStatement(sqls[i]))
             {
                ps.setInt(1, rowId);

                try (ResultSet res = ps.executeQuery())
                {
                   while (res.next())
                   {
                      handleSqlNormalizedRow(res);
                   }
                }
             }
          }
       }

       /**
        * Iterates through the columns in the associated with the {@link ResultSet} and if the column exists in
        * {@link RecordBufferSerializer#sqlNameNormalizedFields}, adds the value in the List from
        * {@link RecordBufferSerializer#currentRowObject} for this extent.
        * NOTE: this method assumes the {@link RecordBufferSerializer#currentRowObject} already contains the
        * field name and the associated value is a List. This method does not handle scenarios when this is not
        * true.
        *
        * @param resultSet
        *        The current {@code ResultSet}
        *
        * @throws SQLException
        *         If a database access error occurs
        */
       @SuppressWarnings("unchecked")
       private void handleSqlNormalizedRow(ResultSet resultSet)
       throws SQLException
       {
          ResultSetMetaData metaData = resultSet.getMetaData();
          int columnCount = metaData.getColumnCount();

          for (int i = 1; i <= columnCount; i++)
          {
             String columnName = metaData.getColumnLabel(i).toLowerCase();
             Property field = sqlNameNormalizedFields.get(columnName);
             if (field == null)
             {
                continue;
             }

             List<String> currentValuesForExtent = (List<String>) currentRowObject.get(getFieldName(field));

             String value = getFullValueForField(resultSet, field);
             currentValuesForExtent.add(value);
          }
       }

       /**
        * Gets the field name based on the flag {@link RecordBufferSerializer#useLegacyFieldsName}.
        *
        * @param field
        *        The field for which to the get name
        *
        * @return See above
        */
       private String getFieldName(Property field)
       {
          if (useLegacyFieldsName)
          {
             return field.legacy;
          }
          return field.column;
       }
    }