OrmUtils.java

/*
** Module   : OrmUtils.java
** Abstract : ORM-related utility methods.
**
** Copyright (c) 2020-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM  20200731 First revision.
** 002 CA  20200921 Added getField(record, index).
**     OM  20201001 Improved DMO manipulation performance by caching slow Property annotation access.
**     AIL 20201210 Added setAllFields for bulk copy of fields.
**     OM  20210223 Invalidate index-based cache in the event of low-level SQL operations.
**     ECF 20210423 BaseRecord API name change.
**     TJD 20220504 Upgrade do Java 11 minor changes.
** 003 OM  20240215 Dropping the records in dropUniqueIndexConflicts() is done only after evicting them, if
**                  exist, from Session.cache.
** 004 OM  20241128  Multi-tenant runtime support: selected the proper persistence context, eventually
**                   based on [sharedDb] parameter.
*/

/*
** 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 com.goldencode.p2j.persist.*;
import com.goldencode.p2j.util.*;
import java.math.*;
import java.util.*;

/**
 * This abstract class groups together a set of static methods that need access to ORM data member only
 * visible from within this package. The methods added here are always static and the parameters must provide
 * all context necessary for its execution
 */
public abstract class OrmUtils
{
   /**
    * Given a TEMP-TABLE record, this method drops all records from the table that are in conflict with the
    * record stored in the {@code buffer} so that at the next flush (insert) it is guaranteed there will be
    * no unique validation issues. 
    * <p>
    * The method works by executing a series of {@code n} DELETE statements, each one for a specific unique
    * index of the record's table.
    * <p>
    * Note: the method deletes all records for a specified {@code multiplex}, INCLUDING the record in the
    *          buffer, if it was already saved;
    *       if multiple existing records conflict with the one stored in buffer, all of them are dropped.
    * 
    * @param   buffer
    *          The buffer that stores the record.
    * @param   p
    *          The persistence to be used when executing DELETE SQL statements.
    * @param   multiplexID
    *          The temp-table multiplexer.
    *
    * @throws  PersistenceException
    *          when a problem is encountered during the process.
    */
   public static void dropUniqueIndexConflicts(RecordBuffer buffer, Persistence p, int multiplexID)
   throws PersistenceException
   {
      DmoMeta dmoInfo = buffer.getDmoInfo();
      RecordMeta metadata = dmoInfo.getRecordMeta();
      PropertyMeta[] props = metadata.getPropertyMeta(false);
      String[] collisionQueries = metadata.getIndexKeyCollisionsSQLs(p.getDialect());
      BitSet[] uIndices = metadata.getUniqueIndices();
      boolean sharedDb = !dmoInfo.multiTenant;
      boolean tx = false;
      
      try
      {
         tx = p.beginTransaction(sharedDb);
         int len = collisionQueries.length;
         for (int i = 0; i < len; i++)
         {
            String sql = collisionQueries[i];
            BitSet uIndex = uIndices[i];
            Object[] args = new Object[uIndex.cardinality() + 1];
            args[0] = multiplexID;
            int argc = 1;
            
            for (int k = uIndex.nextSetBit(0); k >= 0; k = uIndex.nextSetBit(k + 1))
            {
               Object datum = buffer.getCurrentRecord().data[k];
               if (props[k].getType() == character.class)
               {
                  datum = TextOps.trim((String) datum, " ", false, true);
                  if (!props[k].isCaseSensitive())
                  {
                     datum = ((String) datum).toUpperCase();
                  }
               }
               else if (props[k].getType() == decimal.class)
               {
                  // must round [datum] to field's decimals
                  datum = ((BigDecimal) datum).setScale(props[k].getDecimals(), RoundingMode.HALF_UP);
               }
               args[argc++] = datum;
            }
            
            ScrollableResults<Long> results = p.executeSQLQuery(sql, sharedDb, args);
            if (results.first())
            {
               int recCount = 1;
               do
               {
                  if (recCount > 1)
                  {
                     System.out.println("No more than one result expected in index-key collisions.");
                  }
                  
                  Long pk = results.get(0, Long.class);
                  p.getSession(!dmoInfo.multiTenant)
                   .evictIfExist(dmoInfo.getImplementationClass().getName(), pk);
                  p.executeSQL("delete from " + dmoInfo.sqlTable + " where " + Session.PK + " = ?",
                               sharedDb,
                               new Object[]{pk});
                  
                  recCount++;
               }
               while (results.next());
            }
            results.close();
         }
      }
      finally
      {
         if (tx)
         {
            p.commit(sharedDb);
         }
         
         // make sure the ffc will read these nest time ity is used
         buffer.invalidateFFCache(0);
      }
   }
   
   /**
    * Sets a field on a record. The low-level value is directly assigned to property at {@code index} location
    * in the {@code data} array of the record.
    * 
    * @param   record
    *          The {@code BaseRecord} to be changed.
    * @param   index
    *          The absolute index of the property (including extent offset, if the case).
    * @param   datum
    *          The value to be set (plain value, not wrapped in BDT).
    */
   public static void setField(BaseRecord record, int index, Object datum)
   {
      record.setDatum(index, datum);
   }
   
   /**
    * Sets all fields on a record based on a provided record. The low-level values are directly assigned
    * in the {@code data} array of the record.
    * 
    * @param   src 
    *          The source record from which the data will be copied.
    * @param   dst
    *          The destination record to which the data will be copied.
    */
   public static boolean setAllFields(BaseRecord src, BaseRecord dst)
   {
      return dst.setAllData(src);
   }
   
   /**
    * Get the field on the given index, from the specified record.
    * 
    * @param    record
    *           The record instance.
    * @param    index
    *           The absolute index of the property (including extent offset, if the case).
    *           
    * @return   The field's value (plain value, not wrapped in BDT).
    */
   public static Object getField(BaseRecord record, int index)
   {
      return record.getDatum(index);
   }
}