PresortCompoundQuery.java

/*
** Module   : PresortCompoundQuery.java
** Abstract : Implementation of a presorting, multi-table query
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 ECF 20080112   @36991 Created initial version. Implementation of a
**                           presorting, multi-table query which does not
**                           perform its table join(s) on the database
**                           server.
** 002 ECF 20090313   @41602 Forced full-records mode. Added overridden
**                           addComponent() method which calls
**                           setFullRecords().
** 003 SVL 20090723   @43343 Changes due to Sorter API change. 
** 004 SIY 20100303   @44700 Added support for proper accumulator handling.
** 005 SVL 20111111          Make the class implement PresortDelegate.
** 006 ECF 20131028          Import change.
** 007 ECF 20150327          Javadoc fix.
** 008 OM  20150727          Synchronized the cursors for first/next/prev/last methods.
** 009 ECF 20150806          Re-ordered some methods to comply with coding standards.
** 010 SVL 20160526          Removed sorted result set (second). All sorted data is now stored in
**                           the cursor.
** 011 GES 20160919          All initialization is now done with a default constructor followed
**                           by a call to initialize(). This enables query instance members in
**                           converted code to be local variables that are effectively final.
**                           This is needed to support lambdas without moving queries to be
**                           instance members. Queries as instance members breaks recursion
**                           use cases.
** 012 OM  20180620          Added support for dynamically configured sort criteria.
** 013 ECF 20181106          Added runtime support for {FIRST|LAST}-OF methods.
** 014 ECF 20200906          New ORM implementation.
** 015 IAS 20230717          Added 'initialize' with two arguments.
** 016 IAS 20230921          Added initializa2f() method.
** 017 ES  20240404          Overridden isPesort method.
** 018 CA  20240422          Allow the query to be reset to its initialization state, so the same instance can
**                           be used to execute child buffer FILL operations.
** 019 AL2 20240926          Adapted CursorResults to new signature of get.
** 020 LS  20241101          Adapted Presorter to new signature of createSortedResults.
*/

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

/**
 * PresortCompoundQuery is used when it is necessary to presort the results
 * of a multi-table join, where the join cannot be performed at the database
 * server.  This is necessary when tables are being joined across databases,
 * for example, though there may be other circumstances which could drive
 * this requirement.
 * <p>
 * Upon the first request for a record, this query preselects all of its
 * results in the order prescribed by each of its individual query components.
 * It then sorts these results according to the sort criteria provided by one
 * of the <code>addSortCriterion()</code> method variants.  These results are
 * cached.  The appropriate record is returned for the original request, and
 * thereafter, the cache is accessed for all subsequent record retrieval
 * requests.  The cache contains only primary key ID values;  the actual
 * records associated with these are retrieved on demand.
 */
public final class PresortCompoundQuery
extends CompoundQuery
implements Presortable,
           PresortDelegate
{
   /** Helper which sorts preselected, unsorted results */
   private Presorter sorter;
   
   /** The cached results stored in the cursor, wrapped by {@link Results} interface. */
   private CursorResults cursorResults = null;
   
   /**
    * Default constructor. Initialization is deferred until <code>initialize()</code> is
    * called.
    */
   public PresortCompoundQuery()
   {
   }
   
   /**
    * Clear the current results from the query, so the same query instance can be executed again, without 
    * having to {@link #open() re-open} it.
    */
   @Override
   public void clearResults()
   {
      super.clearResults();
      
      this.cursorResults = null;
      this.sorter = null;
   }
   
   /**
    * Initialization logic.
    * <p>
    * This code is intentionally separated from the constructor. The purpose of this separation
    * is to allow construction to occur in the prior scope while initialization can still occur
    * within a method or lambda that is inside the next block's scope.
    *
    * @param    resolveOnce
    *           {@code true} to resolve arguments once, up front, or on every loop cycle. 
    * @param    preselect
    *           {@code true} to force preselection of all results; {@code false} otherwise.
    * @return   This query instance.
    */
   public PresortCompoundQuery initialize(boolean resolveOnce, boolean preselect)
   {
      // Resolve args once up front and preselect results.
      super.initialize(resolveOnce, preselect);
      
      sorter = new Presorter(this);

      setScrolling(); // we need cursor in any case, so set the query scrolling even if it
                      // backs a cycle
                      
      return this;
   }
   /**
    * Initialization logic.
    * <p>
    * This code is intentionally separated from the constructor. The purpose of this separation
    * is to allow construction to occur in the prior scope while initialization can still occur
    * within a method or lambda that is inside the next block's scope.
    *
    * @return   This query instance.
    */
   public PresortCompoundQuery initialize()
   {
      return initialize(true, true);
   }
   
   /**
    * Initialization logic.
    * <p>
    * This code is intentionally separated from the constructor. The purpose of this separation
    * is to allow construction to occur in the prior scope while initialization can still occur
    * within a method or lambda that is inside the next block's scope.
    *
    * @return   This query instance.
    */
   public PresortCompoundQuery initialize2f()
   {
      return initialize(false, false);
   }

   /**
    * Add a joinable query to the compound query, and specify the nature of
    * the iteration which will be performed (first, last, next, etc.) on that
    * component, when the compound query is iterated.  If this is not the
    * first component to be added to the query, the type of join performed
    * with the immediately previous component is determined by the value of
    * <code>outer</code>.
    *
    * @param   query
    *          Query which will perform the fundamental work for this
    *          component when the compound query is iterated.
    * @param   iteration
    *          A {@link QueryConstants constant} indicating the type of work
    *          to be performed upon an iteration of the compound query.  This
    *          parameter determines whether the underlying query will be asked
    *          to perform a first, last, next, previous, or unique record
    *          retrieval.
    * @param   outer
    *          <code>true</code> if this component should be joined to the
    *          previously added component via a left outer join;
    *          <code>false</code> if an inner join is required.  Note that
    *          adding an inner join component will cause any previously added
    *          components added as outer joins to be reset to inner joins.
    *
    * @throws  IllegalStateException
    *          if this method is invoked during an iteration cycle.
    *
    * @see     #iterate
    */
   public void addComponent(Joinable query, int iteration, boolean outer)
   {
      query.setFullRecords();
      
      super.addComponent(query, iteration, outer);
   }
   
   /**
    * Access the record buffers manipulated by this query, in order from
    * left-most (outermost) table of the query's join (if any) to right-most
    * (innermost) table.
    * 
    * @return  Array of one or more record buffers.
    */
   public RecordBuffer[] getRecordBuffers()
   {
      RecordBuffer[] buffers = new RecordBuffer[getTableCount()];
      Iterator<RecordBuffer> iter = recordBuffers();
      for (int i = 0; iter.hasNext(); i++)
      {
         buffers[i] = iter.next();
      }
      
      return buffers;
   }
   
   /**
    * Add a single sort criterion, in the form of a <code>Resolvable</code>
    * object which is resolved at sort time.  It is the value of the resolved
    * result which is used for the sort.
    * <p>
    * The sort is ascending, from lowest resolved value to highest.
    *
    * @param   sort
    *          A resolvable object whose resolved result is used for sorting.
    */
   public void addSortCriterion(Resolvable sort)
   {
      sorter.addSortCriterion(sort);
   }
   
   /**
    * Add a single sort criterion, in the form of a <code>Resolvable</code>
    * object which is resolved at runtime.  It is the value of the resolved
    * result which is used for the sort.
    *
    * @param   sort
    *          A resolvable object whose resolved result is used for sorting.
    * @param   descending
    *          if <code>true</code>, the sort is descending, from highest
    *          resolved value to lowest;  otherwise, the sort is ascending.
    */
   public void addSortCriterion(Resolvable sort, boolean descending)
   {
      sorter.addSortCriterion(sort, descending);
   }
   
   /**
    * Add a single dynamic sort criterion, in the form of a {@code Resolvable} object which is
    * resolved at runtime.  It is the value of the resolved result which is used for the sort.
    * <p> 
    * Unlike {@link #addSortCriterion} methods, the criterion is added with higher priority.
    *
    * @param   sort
    *          A resolvable object whose resolved result is used for sorting.
    * @param   ascending
    *          if {@code true}, the sort is ascending, from lowest resolved value to highest;
    *          otherwise, the sort is descending.
    */
   @Override
   public void addDynamicSortCriterion(Resolvable sort, boolean ascending)
   {
      sorter.addDynamicSortCriterion(sort, ascending);
   }
   
   /**
    * Clears all sort criteria added dynamically.
    */
   @Override
   public void clearDynamicSortCriteria()
   {
      sorter.clearDynamicSortCriteria();
   }
   
   /**
    * Enable all sort criteria set for this query to act as break group
    * categories.  When the query's results are presorted, the contents of
    * each break group are determined.
    */
   public void enableBreakGroups()
   {
      sorter.enableBreakGroups();
   }
   
   /**
    * Determine whether the query result row currently being visited is the
    * first row in the presorted results list.
    *
    * @return  <code>true</code> if the current result row is the first row,
    *          else <code>false</code>.
    */
   public logical isFirst()
   {
      return sorter.isFirst();
   }
   
   /**
    * Determine whether the query result row currently being visited is the
    * first row within a break group whose category is identified by the
    * specified resolvable object.
    *
    * @param   key
    *          Break group category key.  Must be the same object reference
    *          as was specified previously when invoking one of the
    *          <code>addSortCriterion</code> method variants.
    *
    * @return  <code>true</code> if break groups are enabled and current row
    *          is first in the specified break group;
    *          <code>false</code> if break groups have not been enabled or
    *          current row is not first in the specified break group.
    */
   public logical isFirstOfGroup(Resolvable key)
   {
      return sorter.isFirstOfGroup(key);
   }
   
   /**
    * Determine whether the query result row currently being visited is the first row of the
    * query's result set or the first row within a break group whose category is identified by
    * the specified BREAK-BY level.
    *
    * @param   level
    *          Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
    *          criterion, etc. Must not be negative or greater than the number of sort criteria.
    *
    * @return  {@code true} if the current row is first in the result set or within the specified
    *          break group; else {@code false}.
    */
   public logical isFirstOfGroup(integer level)
   {
      return sorter.isFirstOf(level);
   }
   
   /**
    * Determine whether the query result row currently being visited is the first row of the
    * query's result set or the first row within a break group whose category is identified by
    * the specified BREAK-BY level.
    *
    * @param   level
    *          Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
    *          criterion, etc. Must not be negative or greater than the number of sort criteria.
    *
    * @return  {@code true} if the current row is first in the result set or within the specified
    *          break group; else {@code false}.
    */
   public logical isFirstOfGroup(int level)
   {
      return isFirstOfGroup(new integer(level));
   }
   
   /**
    * Determine whether the query result row currently being visited is the
    * last row in the presorted results list.
    *
    * @return  <code>true</code> if the current result row is the last row,
    *          else <code>false</code>.
    */
   public logical isLast()
   {
      return sorter.isLast();
   }
   
   /**
    * Indicate whether the query is of presort type.
    *
    * @return  <code>true</code> if the query is of presort type..
    */
   @Override
   public boolean isPresort()
   {
      return true;   
   }
   
   /**
    * Determine whether the query result row currently being visited is the
    * last row within a break group whose category is identified by the
    * specified resolvable object.
    *
    * @param   key
    *          Break group category key.  Must be the same object reference
    *          as was specified previously when invoking one of the
    *          <code>addSortCriterion</code> method variants.
    *
    * @return  <code>true</code> if break groups are enabled and current row
    *          is last in the specified break group;
    *          <code>false</code> if break groups have not been enabled or
    *          current row is not last in the specified break group.
    */
   public logical isLastOfGroup(Resolvable key)
   {
      return sorter.isLastOfGroup(key);
   }
   
   /**
    * Determine whether the query result row currently being visited is the last row of the
    * query's result set or the last row within a break group whose category is identified by
    * the specified BREAK-BY level.
    *
    * @param   level
    *          Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
    *          criterion, etc. Must not be negative or greater than the number of sort criteria.
    *          Must not be unknown value.
    *
    * @return  {@code true} if the current row is last in the result set or within the specified
    *          break group; else {@code false}.
    */
   public logical isLastOfGroup(integer level)
   {
      return sorter.isLastOf(level);
   }
   
   /**
    * Determine whether the query result row currently being visited is the last row of the
    * query's result set or the last row within a break group whose category is identified by
    * the specified BREAK-BY level.
    *
    * @param   level
    *          Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
    *          criterion, etc. Must not be negative or greater than the number of sort criteria.
    *
    * @return  {@code true} if the current row is last in the result set or within the specified
    *          break group; else {@code false}.
    */
   public logical isLastOfGroup(int level)
   {
      return isLastOfGroup(new integer(level));
   }
   
   /**
    * Access the current, sorted results for this query.
    *
    * @return  <code>Results</code> object used to retrieve sorted results.
    */
   public Results getSortedResults()
   {
      return cursorAsResults();
   }
   
   /**
    * Retrieve the next virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    *
    * @throws  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean next(boolean iterating, LockType lockType)
   {
      boolean hasNext = super.next(iterating, lockType);
      
      accumulate();
      
      return hasNext;
   }
   
   /**
    * Determine whether the query result row currently being visited is the
    * last row within a break group whose category is identified by the
    * specified resolvable object.
    *
    * @param   key
    *          Break group category key.  Must be the same object reference
    *          as was specified previously when invoking one of the
    *          <code>addSortCriterion</code> method variants.
    *
    * @return  <code>true</code> if break groups are enabled and current row
    *          is last in the specified break group;
    *          <code>false</code> if break groups have not been enabled or
    *          current row is not last in the specified break group.
    */
   protected boolean isLastOfBreakGroup(Resolvable key)
   {
      return sorter.isLastOfBreakGroup(key);
   }
   
   /**
    * Determine whether the query result row currently being visited is the
    * first row within a break group whose category is identified by the
    * specified resolvable object.
    * <p>
    * This is the backing worker method for {@link #isFirstOfGroup} and is
    * used internally by the query to manage accumulators.
    *
    * @param   key
    *          Break group category key.  Must be the same object reference
    *          as was specified previously when invoking one of the
    *          <code>addSortCriterion</code> method variants.
    *
    * @return  <code>true</code> if current row is first in a break group;
    *          <code>false</code> if it is not;  unknown value if break groups
    *          have not been enabled.
    */
   protected boolean isNewBreakGroup(Resolvable key)
   {
      return sorter.isNewBreakGroup(key);
   }

   /**
    * Determines if the query is presorted, i.e. if the query is sorted up front in some special
    * way.
    *
    * @return <code>true</code> for this implementation.
    */
   protected boolean isPresorted()
   {
      return true;
   }

   /**
    * Get the cached results stored in the cursor, wrapped by {@link Results} interface.
    *
    * @return the cached results stored in the cursor, wrapped by {@link Results} interface.
    */
   protected CursorResults cursorAsResults()
   {
      if (cursorResults == null)
      {
         cursorResults = new CursorResults(cursor);
      }

      return cursorResults;
   }

   /**
    * Presort the results stored in the cursor.
    */
   protected void presort()
   {
      try
      {
         if (hasAccumulators())
         {
            postponeAccumulation();
         }

         // reset cursor's position, the sorter processes rows from this position
         cursor.beforeFirst();

         // sort them
         SimpleResults sorted = (SimpleResults) sorter.createSortedResults(cursorAsResults(), true);

         // discard unsorted cursor results and set sorted instead
         cursor.setResults(sorted.getRows());
      }
      catch (PersistenceException exc)
      {
         ErrorManager.recordOrThrowError(exc);
      }
      finally
      {
         if (hasAccumulators())
         {
            restoreAccumulation();
         }
      }
   }
   
   /**
    * An implementation of the <code>Results</code> interface which is backed
    * by a dynamic {@link Cursor} object.
    * <p>
    * Only those methods which are required by the {@link Presorter} to sort
    * the results managed by the cursor are implemented.
    
    */
   private static class CursorResults
   implements Results
   {
      /** Backing cursor */
      private final Cursor cursor;
      
      /** Array of records which represents the current, virtual row */
      private Object[] row = null;
      
      /**
       * Constructor which accepts the backing cursor object.
       * 
       * @param   cursor
       *          Cursor which backs this <code>Results</code> object.
       */
      CursorResults(Cursor cursor)
      {
         this.cursor = cursor;
      }
      
      /**
       * Move cursor to the first results row.
       *
       * @return  <code>true</code> if there are any results.
       */
      public boolean first()
      {
         row = cursor.getFirst();
         
         return (row != null);
      }
      
      /**
       * Move cursor to the last results row.
       *
       * @return  <code>true</code> if there are any results.
       */
      public boolean last()
      {
         row = cursor.getLast();
         
         return (row != null);
      }
      
      /**
       * Move cursor to the next results row.
       *
       * @return  <code>true</code> if there is a result under the cursor
       *          under the move.
       */
      public boolean next()
      {
         row = cursor.getNext();
         
         return (row != null);
      }
      
      /**
       * Move cursor to the previous results row.
       *
       * @return  <code>true</code> if there is a result under the cursor
       *          under the move.
       */
      public boolean previous()
      {
         row = cursor.getPrevious();
         
         return (row != null);
      }
      
      /**
       * Is the cursor on the first row in the results set?
       *
       * @return  <code>true</code> if the cursor is on the first row.
       */
      public boolean isFirst()
      {
         return getRowNumber() == 0;
      }
      
      /**
       * Is the cursor on the last row in the results set?
       *
       * @return  <code>true</code> if the cursor is on the first row.
       */
      public boolean isLast()
      {
         return cursor.size().intValue() == getRowNumber() + 1;
      }
      
      /**
       * Returns the row at the current position. It can be either only an array of PK or
       * an array of DMOs. 
       * 
       * @param   forceOnlyId
       *          Indicates whether it's only the primary keys that must be returned, 
       *          or original data found is returned (either PK or DMO).
       *
       * @return  Object array or {@code null}.
       */
      @Override
      public Object[] get(boolean forceOnlyId)
      {
         if (!forceOnlyId)
         {
            return row;
         }
         
         Object[] newRow = new Object[row.length];
         for (int i = 0; i < row.length; i++)
         {
            if (row[i] instanceof Record)
            {
               newRow[i] = ((Record) row[i]).primaryKey();
            }
            else
            {
               newRow[i] = row[i];
            }
         }
         
         return newRow;
      }
      
      /**
       * Get the object at the current result row, at the specified column.
       *
       * @param   column
       *          Zero-based index column of the desired object.
       *
       * @return  Object at <code>column</code> or <code>null</code>.
       */
      public Object get(int column)
      {
         return (row == null ? null : row[column]);
      }
      
      /**
       * Get the primary key ID at the current result row, at the specified
       * column.
       *
       * @param   column
       *          Zero-based index column of the desired ID.
       *
       * @return  ID of the record or <code>null</code>.
       */
      public Long getID(int column)
      {
         Object datum = get(column);
         
         if (datum instanceof Record)
         {
            return ((Record) datum).primaryKey();
         }
         
         return ((Long) datum);
      }
      
      /**
       * Get the row number currently under the cursor.
       *
       * @return  Zero-based index of the current row, or <code>-1</code>
       *          if the cursor is not currently on a result.
       */
      public int getRowNumber()
      {
         integer position = cursor.currentRow();
         if (position.isUnknown())
         {
            return -1;
         }
         
         return position.intValue() - 1;
      }
      
      /**
       * Set the row number currently under the cursor.
       *
       * @param   row
       *          Zero-based index of the row to be set as the current row.
       *
       * @return  <code>true</code> if there is a row at the specified row
       *          number;  else <code>false</code>.
       * 
       * @throws  UnsupportedOperationException
       *          always;  this method is not needed to sort results.
       */
      public boolean setRowNumber(int row)
      {
         throw new UnsupportedOperationException();
      }
      
      /**
       * Scroll the cursor ahead by the specified number of rows.  If the
       * number is negative, the cursor is moved backward.  This may put the
       * cursor off the end (either end) of the query's results.
       *
       * @param   rows
       *          Number of rows to jump ahead or back.
       *
       * @return  <code>true</code> if there is a row at the new location;
       *          else <code>false</code>.
       * 
       * @throws  UnsupportedOperationException
       *          always;  this method is not needed to sort results.
       */
      public boolean scroll(int rows)
      {
         throw new UnsupportedOperationException();
      }
      
      /**
       * Reset the cursor to its natural starting position, before the first
       * result row.
       */
      public void reset()
      {
         cursor.beforeFirst();
      }
      
      /**
       * Invoked when the current Hibernate session is about to close.  Gives
       * the implementation an opportunity to perform appropriate processing
       * in response to this event.
       * <p>
       * This implementation does nothing.
       */
      public void sessionClosing()
      {
      }
      
      /**
       * Clean up and release any resources which this object is holding, such
       * as open result sets.
       * <p>
       * This implementation does nothing.
       */
      public void cleanup()
      {
      }
   }
}