AbstractRowStructure.java

/*
** Module   : AbstractRowStructure.java
** Abstract : Defines an abstract row structure in a query.
**
** Copyright (c) 2020-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
** 001 AL2 20231128 Initial version.
**     AL2 20231211 Recieve the DMO meta directly in the constructor.
**     AL2 20231212 Use dmoMeta directly, without getter. Reworked toString.
** 002 TJD 20240131 Check SQLException for Interrupted exception cause
** 003 LS  20241105 Added hasOnlyPK().
*/

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

import java.sql.*;
import java.util.Iterator;

import com.goldencode.p2j.persist.DataModelObject;
import com.goldencode.p2j.persist.PersistenceException;
import com.goldencode.p2j.util.StopConditionException;

/**
 * Common behavior for all row structures. This aggregates all routines that are common
 * like non-expanded extent hydration and non-expanded property detection. 
 */
public abstract class AbstractRowStructure
implements RowStructure
{

   /** The DMO class. */
   protected final Class<? extends DataModelObject> dmoClass;

   /** The DMO meta of the DMO class */
   protected final DmoMeta dmoMeta;
   
   /** Check if this row structure should also hydrate the non-expanded extent fields. */
   protected boolean hasExtents = false;

   /** A cached value of the property meta array that can be found in the record meta */
   private PropertyMeta[] pm;
   
   /**
    * Create a new instance.
    * 
    * @param    dmoMeta
    *           The DMO metadata of the underlying DMO for this structure.
    */   
   public AbstractRowStructure(DmoMeta dmoMeta)
   {
      this.dmoMeta = dmoMeta;
      this.dmoClass = dmoMeta.dmoImplInterface;
   }
   
   /**
    * The DMO class whose properties are included in this structure. 
    * 
    * @return   The DMO class underlying this structure.
    */
   @Override
   public Class<? extends DataModelObject> getDmoClass()
   {
      return dmoClass;
   }
   
   /**
    * Append a property to the row structure. This doesn't check if the property actually belongs to the
    * DMO class used for this structure. 
    * 
    * This is implemented in the abstract class to uniformly handle non-expanded extent fields.
    * 
    * @param   prop 
    *          The property to be appended to the structure.
    *          
    * @return  {@code true} if adding the prop to the row structure succeeded
    *          If there is some invariant on the structure, adding props may fail.
    */
   @Override
   public boolean addProperty(Property prop)
   {
      if (prop != null && prop.extent != 0 && prop.index == 0)
      {
         hasExtents = true;
         return true;
      }
      
      return false;
   }
   
   /**
    * Check if this structure requires a hydration of non-expanded properties. This will only return true
    * if a non-expanded property was ever added through {@link #addProperty} or {@link #forceExtents()} was
    * called.
    * 
    * @return   {@code true} if this structure will also hydrate non-expanded properties.
    */
   public boolean hasExtents()
   {
      return hasExtents;
   }
   
   /**
    * Force the non-expanded extents to be hydrated.
    */
   public void forceExtents()
   {
      hasExtents = true;
   }
   
   /**
    * Hydrate record's non-expanded extents.
    * 
    * @param   session
    *          The session to be used to query the non-expanded extent tables.
    * @param   r
    *          The record to be hydrated.
    * @param   recOffset
    *          The offset to which the extent data should be added into the record. 
    * 
    * @throws  PersistenceException
    *          thrown when hydrating extents fails (as it requires extra SQL queries to be run).
    */
   protected void hydrateExtents(Session session, BaseRecord r, int recOffset)
   throws PersistenceException
   {
      // TODO: collect extent properties and do a new query from SQL, see Loader.readExtentData()
      try
      {
         Connection conn = session.getConnection();
         RecordMeta recordMeta = r._recordMeta();
         PropertyMeta[] propsMeta = recordMeta.getPropertyMeta(false);
         String[] loadSqls = recordMeta.loadSql;
         int[] loadSqlsIndexes = recordMeta.loadSqlIndexes;
         boolean incomplete = isIncomplete();
         
         // only use the extent queries, skip the loadSqls[0] which contains the simple properties
         for (int i = 1; i < loadSqls.length; i++) // skip "plain" columns 
         {            
            if (incomplete)
            {
               // resolve the offset in the 'props' array where this sql starts.  
               // all non-expanded extent fields are loaded even for incomplete records.
               recOffset = loadSqlsIndexes[i];
            }
            
            ResultSet rs = null;
            try (PreparedStatement ps = conn.prepareStatement(loadSqls[i]))
            {
               // set the [parent_id] parameter
               ps.setLong(1, r.primaryKey());
               rs = ps.executeQuery();
               recOffset = Loader.readExtentData(r, rs, propsMeta, recOffset);
            }
            finally
            {
               if (rs != null)
               {
                  rs.close();
               }
            }
         }
      }
      catch (SQLException e)
      {
         // extract Interrupted Exception out of SQLException
         if (e instanceof SQLException && e.getCause() instanceof InterruptedException)
         {
            throw new StopConditionException("Failed to populate extents for class " + dmoClass.getName(), e);
         }
         else 
         {
            throw new PersistenceException("Failed to populate extents for class " + dmoClass.getName(), e);
         }
      }
   }

   /**
    * Check if this structure contains only the primary key.
    * @return   {@code true} if PK is the only property.
    */
   protected boolean hasOnlyPK()
   {
      boolean hasOnlyPK = false;
      if (getCount() == 1)
      {
         Iterator<Property> props = getProps();
         if (props.hasNext())
         {
            Property prop = props.next();
            hasOnlyPK = prop.id == ReservedProperty.ID_PRIMARY_KEY;
         }

      }
      return hasOnlyPK;
   }
   
   /**
    * Obtain a short description of this object used for debugging. 
    *
    * @return  a short description of this object used for debugging. 
    */
   @Override
   public String toString()
   {
      StringBuilder sb = new StringBuilder(dmoClass.getName() + "{\n");
      sb.append("   count : " + getCount()).append("\n");
      sb.append("   not expanded extents : " + hasExtents).append("\n");
      sb.append("   props : [\n");
      Iterator<Property> it = getProps();
      while (it.hasNext())
      {
         sb.append("      ").append(it.next().name).append("\n");
      }
      sb.append("]}");
      return sb.toString();
   }
   
   /**
    * Retrieve the DMO meta for the provided DMO class. This should be initialized lazily, as not all
    * implementations require the DMO meta.
    *
    * @return   The meta of the underlying DMO class.
    */
   @Override
   public DmoMeta getDmoMeta()
   {
      return this.dmoMeta;
   }
   
   /**
    * Check if this row structure is for a temporary table.
    *
    * @return {@code true} if this DMO structure is for a temporary table.
    */
   protected boolean isTempTable()
   {
      return dmoMeta.tempTable;
   }
   
   /**
    * The property meta array - lazily computed based on the dmo meta.
    *
    * @return   the array of property meta data.
    */
   protected PropertyMeta[] getPropertyMeta()
   {
      if (pm == null)
      {
         this.pm = dmoMeta.recordMeta.getPropertyMeta(false);
      }
      
      return this.pm;
   }
}