DynamicQuery.java

/*
** Module   : DynamicQuery.java
** Abstract : Base class for queries which fetch records progressively
**
** Copyright (c) 2005-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051215   @23741 Created initial version. Primary purpose at
**                           this time is to centralize logic and data
**                           related to scrollable cursor support.
** 002 ECF 20060104   @23823 Changed hierarchy. Extends AbstractQuery base
**                           class.
** 003 ECF 20060315   @25076 Added safety code to setScrolling. It is now
**                           tolerant of multiple invocations; a query
**                           cursor is created only on the first call.
** 004 ECF 20060501   @25914 Added isPreselect method. Indicates query's
**                           preselection behavior.
** 005 ECF 20060801   @28350 Added abstract getBreakValue() method.
** 006 ECF 20061102   @30957 Added support for reposition notification and
**                           auto-fetch features.
** 007 ECF 20061206   @31586 Integrated changes to AbstractQuery's
**                           afterReposition() signature. Leverage new
**                           return value from Cursor's reposition()
**                           variant which accepts an array of IDs, to
**                           ensure reposition listeners are notified of
**                           default reposition in the event of an error.
** 008 ECF 20070518   @33680 Implement RecordChangeListener interface.
**                           This object is now registered as a listener
**                           with the ChangeBroker.
** 009 ECF 20070522   @33723 Fixed stateChanged() method. Removed throw of
**                           IllegalStateException. The logic for this
**                           exception was incorrect.
** 010 ECF 20080122   @36886 Added resetScrolling(). Replaces cursor with
**                           a new instance.
** 011 ECF 20080201   @36999 Fixed currentRow(). Return unknown if query
**                           is not scrolling, rather than throwing an
**                           exception.
** 012 SVL 20080604   @38532 Modified according to the new P2JQuery interface.
** 013 ECF 20080729   @39319 Added rowid support. Refactored reposition*()
**                           methods.
** 014 ECF 20081106   @40322 Fixed stateChanged(), added _isOffEnd().
** 015 ECF 20090521   @42979 Changes to fix incorrect behavior. Removed
**                           RecordChangeListener interface and stateChanged()
**                           methods. Track how many times the current result
**                           row has changed and report this as the current
**                           row number for non-scrolling queries.
** 016 ECF 20090815   @43632 Explicitly invoke superclass default c'tor in
**                           this default c'tor.
** 017 SVL 20140909          Added isNativelyPreselect and isScrolling.
** 018 SVL 20150821          isScrolling was made public.
** 019 SVL 20160608          Moved reposition validation from Cursor to this class.
** 020 SVL 20160629          Reset cursor on query (re)open.
** 021 OM  20170209          Added return value for repositioning methods.
** 022 OM  20190720          Added repositionByID() for internal usage.
** 023 SVL 20191218          Handled case when REPOSITION TO ROWID fails.
** 024 ECF 20200906          New ORM implementation.
** 025 OM  20201120          Fixed error message.
** 026 AL2 20210909          Added support for indexed reposition; reset query if reposition fails.
** 027 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.
** 028 DDF 20230717          Only allow scrolling when forwardOnly is false.
**     DDF 20230721          Added setForwardOnly() method.
**     DDF 20240130          Do not reset scrolling when setting the forwardOnly flag.
** 029 AL2 20250415          If the query is indexed-reposition, check only the cache before resetting.
*/

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

/**
 * Abstract base class for query classes which dynamically navigate and
 * retrieve results, and which must support results list scrolling.
 * <p>
 * The primary purpose of this class is to manage a query cursor for results
 * list scrolling and repositioning, and to provide an API to client code to
 * perform these tasks, and to interrogate the cursor for state data.
 * <p>
 * This class implements the <code>RecordChangeListener</code> interface.
 * When the {@link #setScrolling} method is invoked, this object is registered
 * as a listener with the context-local {@link ChangeBroker} instance, so
 * that it can receive notifications of record deletions.  If the deleted
 * record was in this query's cache, the cache is expired.  Subclasses must
 * implement the {@link
 * com.goldencode.p2j.persist.event.RecordChangeListener#recordBuffers
 * recordBuffers()} method.
 */
abstract class DynamicQuery
extends AbstractQuery
implements Scrollable
{
   /** Number of times the query's position has moved (non-scrolling only) */
   private int moveCount = 0;
   
   /** Cursor which scrolls query results */
   protected Cursor cursor = null;
   
   /**
    * Default constructor.
    */
   protected DynamicQuery()
   {
      super();
   }
   
   /**
    * 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()
   {
      this.moveCount = 0;
      this.cursor = null;
   }
   
   /**
    * Create a query cursor to enable results list scrolling/repositioning.
    * Only one cursor is created, even if this method is invoked multiple
    * times.
    */
   public void setScrolling()
   {
      if (!isScrolling())
      {
         resetScrolling();
      }
   }
   
   /**
    * Set the forward-only attribute to the given value and resets the <code>cursor</code>
    * if necessary.
    * 
    * @param   forwardOnly
    *          <code>true</code> to enable forward-only, <code>false</code> to disable it.
    */
   @Override
   public void setForwardOnly(boolean forwardOnly)
   {
      super.setForwardOnly(forwardOnly);
      if (forwardOnly)
      {
         cursor = null;
      }
   }
   
   /**
    * Reposition the cursor such that a request to retrieve the next result
    * will retrieve the result which matches the specified primary key ID.  A
    * request to retrieve the previous result will retrieve the result
    * immediately previous to this, if any, in the results list.
    *
    * @param   id
    *          A primary key ID, representing the target record (in the case
    *          of a join, this ID is associated with the left-most record in
    *          the join).
    * 
    * @throws  ErrorConditionException
    *          if the specified ID represents the unknown value.
    */
   public void repositionByID(recid id)
   {
      verifyScrolling();
      ErrorConditionException error = null;
      boolean permitFetch = true;
      integer oldCurrentRow = currentRow();
      
      try
      {
         permitFetch = cursor.repositionByID(!indexedReposition, id);
         if (!permitFetch && indexedReposition)
         {
            // if indexed reposition is set, reset the query and try again
            reset();
            permitFetch = cursor.repositionByID(true, id);
         }
      }
      catch (ErrorConditionException exc)
      {
         permitFetch = false;
         error = exc;
      }
      finally
      {
         afterReposition(permitFetch, oldCurrentRow);
      }
      
      if (error != null)
      {
         throw error;
      }
   }
   
   /**
    * Reposition the cursor such that a request to retrieve the next result
    * will retrieve the result which matches the specified array of primary
    * key IDs.  A request to retrieve the previous result will retrieve the
    * result immediately previous to this, if any, in the results list.
    *
    * @param   id1
    *          A primary key ID, representing the target record (in the case
    *          of a join, this ID is associated with the left-most record in
    *          the join).
    * @param   joinIDs
    *          All remaining primary key IDs, if any, arranged from left to
    *          right to coincide with the records being joined by the
    *          underlying query.
    * 
    * @throws  ErrorConditionException
    *          if any of the specified IDs represents the unknown value.
    */
   public void repositionByID(rowid id1, rowid...joinIDs)
   {
      verifyScrolling();
      integer oldCurrentRow = currentRow();
      boolean permitFetch = cursor.repositionByID(!indexedReposition, id1, joinIDs);
      if (!permitFetch && indexedReposition)
      {
         // if indexed reposition is set, reset the query and try again
         reset();
         permitFetch = cursor.repositionByID(true, id1, joinIDs);
      }
      afterReposition(permitFetch, oldCurrentRow);
      
      if (!permitFetch)
      {
         ErrorManager.recordOrThrowError(7331, "", "");
         // Cannot reposition query <name> to recid/rowid(s) given.
      }
   }
   
   /**
    * Reposition the cursor such that a request to retrieve the next result will retrieve the
    * result which matches the specified array of primary key IDs. A request to retrieve the
    * previous result will retrieve the result immediately previous to this, if any, in the
    * results list.
    * <strong>Note:</strong> This is not public API.
    *
    * @param   serIDs
    *          A set of primary key IDs, arranged from left to right to coincide with the records
    *          being joined by the underlying query.
    *
    * @throws  ErrorConditionException
    *          if any of the specified IDs represents the unknown value.
    *
    * @return  {@code true} on success.
    */
   @Override
   public boolean repositionByID(Long... serIDs)
   {
      verifyScrolling();
      integer oldCurrentRow = currentRow();
      boolean permitFetch = cursor.reposition(serIDs, !indexedReposition);
      if (!permitFetch && indexedReposition)
      {
         // if indexed reposition is set, reset the query and try again
         reset();
         permitFetch = cursor.reposition(serIDs, true);
      }
      afterReposition(permitFetch, oldCurrentRow);
      
      if (!permitFetch)
      {
         ErrorManager.recordOrThrowError(7331, "", "");
         // Cannot reposition query <name> to recid/rowid(s) given.
      }
      
      return permitFetch; 
   }
   
   /**
    * Reposition the cursor to the specified row in the result set.  The row
    * indicates the position between two results, such that a call to
    * retrieve the next record will get the result at the specified row,
    * and a call to retrieve the previous result will get the result before
    * the new position.
    *
    * @param   row
    *          1-based index of the target position.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query;
    *          if <code>row</code> represents unknown value.
    */
   public void reposition(NumberType row)
   {
      verifyScrolling();
      cursor.reposition(row);
      afterReposition(true);
   }
   
   /**
    * Reposition the query to the specified row in the result set.  The row
    * indicates the position between two results, such that a call to
    * retrieve the next record will get the record after the new position,
    * and a call to retrieve the previous record will get the record before
    * the new position.
    *
    * @param   row
    *          1-based index of the target position.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query.
    */
   public void reposition(int row)
   {
      verifyScrolling();
      cursor.reposition(row);
      afterReposition(true);
   }
   
   /**
    * Advance the current cursor position forward by the specified number of
    * rows.  This will always leave the cursor between two results.  For
    * example, given a request to move 1 row forward:
    * <ul>
    *   <li>if the cursor currently is positioned <i>directly on a result</i>,
    *       this would move it ahead <i>one and a half positions</i>, to just
    *       beyond the following result;
    *   <li>if the cursor currently is positioned <i>between two results</i>,
    *       this would move it ahead <i>one position</i>, to just beyond the
    *       following result.
    * </ul>
    *
    * @param   rows
    *          Number of rows to scroll the cursor forward.
    *
    * @return  {@code true} if operation was successful and the cursor was moved the exact amount
    *          of rows as specified by the parameter. If {@code false} is returned then it is
    *          possible that cursor's position was moved but not with {@code rows} rows.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query;
    *          if <code>rows</code> represents the unknown value.
    */
   public boolean forward(NumberType rows)
   {
      verifyScrolling();
      boolean moved = cursor.forward(rows);
      afterReposition(true);
      return moved;
   }
   
   /**
    * Advance the current cursor position forward by the specified number of
    * rows.  This will always leave the cursor between two results.  For
    * example, given a request to move 1 row forward:
    * <ul>
    *   <li>if the cursor currently is positioned <i>directly on a result</i>,
    *       this would move it ahead <i>one and a half positions</i>, to just
    *       beyond the following result;
    *   <li>if the cursor currently is positioned <i>between two results</i>,
    *       this would move it ahead <i>one position</i>, to just beyond the
    *       following result.
    * </ul>
    *
    * @param   rows
    *          Number of rows to scroll the cursor forward.
    *
    * @return  {@code true} if operation was successful and the cursor was moved the exact amount
    *          of rows as specified by the parameter. If {@code false} is returned then it is
    *          possible that cursor's position was moved but not with {@code rows} rows.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query.
    */
   public boolean forward(int rows)
   {
      verifyScrolling();
      boolean moved = cursor.forward(rows);
      afterReposition(true);
      return moved;
   }
   
   /**
    * Move the current cursor position backward by the specified number of
    * rows.  This will always leave the cursor between two results.  For
    * example, given a request to move 1 row backward:
    * <ul>
    *   <li>if the cursor currently is positioned <i>directly on a result</i>,
    *       this would move it back <i>one half position</i>, to just
    *       before the current result;
    *   <li>if the cursor currently is positioned <i>between two results</i>,
    *       this would move it back <i>one position</i>, to just before the
    *       current result.
    * </ul>
    *
    * @param   rows
    *          Number of rows to scroll the cursor backward.
    *
    * @return  {@code true} if operation was successful and the cursor was moved the exact amount
    *          of rows as specified by the parameter. If {@code false} is returned then it is
    *          possible that cursor's position was moved but not with {@code rows} rows.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query;
    *          if <code>rows</code> represents the unknown value.
    */
   public boolean backward(NumberType rows)
   {
      verifyScrolling();
      boolean moved = cursor.backward(rows);
      afterReposition(true);
      return moved;
   }
   
   /**
    * Move the current cursor position backward by the specified number of
    * rows.  This will always leave the cursor between two results.  For
    * example, given a request to move 1 row backward:
    * <ul>
    *   <li>if the cursor currently is positioned <i>directly on a result</i>,
    *       this would move it back <i>one half position</i>, to just
    *       before the current result;
    *   <li>if the cursor currently is positioned <i>between two results</i>,
    *       this would move it back <i>one position</i>, to just before the
    *       current result.
    * </ul>
    *
    * @param   rows
    *          Number of rows to scroll the cursor backward.
    *
    * @return  {@code true} if operation was successful and the cursor was moved the exact amount
    *          of rows as specified by the parameter. If {@code false} is returned then it is
    *          possible that cursor's position was moved but not with {@code rows} rows.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query.
    */
   public boolean backward(int rows)
   {
      verifyScrolling();
      boolean moved = cursor.backward(rows);
      afterReposition(true);
      return moved;
   }
   
   /**
    * Get the <b><i>current</i></b> size of the cached results list.  However,
    * since this cache is built progressively as the query is scrolled,
    * repositioned, and naturally navigated, this <b>does not</b> necessarily
    * represent the full count of results which may satisfy the query.
    *
    * @return  Current results list size.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query.
    */
   public integer size()
   {
      verifyScrolling();
      
      return cursor.size();
   }
   
   /**
    * Get the 1-based index of the current row in the result set.  This is
    * the index of the entry at or before which the cursor currently is
    * positioned.
    *
    * @return  Current row or the unknown value if the current row cannot be
    *          determined.  The current row cannot be determined if the
    *          results list is empty, or if the cursor currently is in
    *          backward scroll mode.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query.
    */
   public integer currentRow()
   {
      return currentRowImpl();
   }
   
   /**
    * Get the 1-based index of the current row in the result set.  This is
    * the index of the entry at or before which the cursor currently is
    * positioned.
    *
    * @return  Current row or the unknown value if the current row cannot be
    *          determined.  The current row cannot be determined if the
    *          results list is empty.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query.
    */
   public integer currentRowImpl()
   {
      if (!isScrolling())
      {
         return (moveCount > 0 ? new integer(moveCount) : new integer());
      }
      
      return cursor.currentRow();
   }
   
   /**
    * Indicate whether the cursor has run off one or the other end of the
    * associated query's list of available results.  If <code>true</code>,
    * this indicates that the query cannot produce any more results in the
    * current scroll direction.
    *
    * @return  <code>true</code> if the cursor is off either end of its
    *          query's results list.
    *
    * @throws  ErrorConditionException
    *          if this query is not a scrolling query.
    */
   public boolean _isOffEnd()
   {
      if (isScrolling())
      {
         return cursor.isOffEnd(null);
      }
      
      return !OffEnd.NONE.equals(getOffEnd());
   }
   
   /**
    * Indicate whether this query preselects all of its results.
    *
    * @return  <code>false</code>.
    */
   public boolean isPreselect()
   {
      return false;
   }

   /**
    * Indicate whether this query is a preselect query by its nature.
    *
    * @return  <code>true</code>.
    */
   public boolean isNativelyPreselect()
   {
      return false;
   }

   /**
    * Indicate whether this query's <code>scrolling</code> flag has been set.
    *
    * @return  <code>true</code> if this is a scrolling query;  else
    *          <code>false</code>.
    */
   public boolean isScrolling()
   {
      return cursor != null && !isForwardOnly();
   }

   /**
    * Open the prepared query and reset the cursor.
    */
   public void open()
   {
      if (cursor != null)
      {
         cursor.reset();
      }

      super.open();
   }

   /**
    * Create a query cursor to enable results list scrolling/repositioning.
    */
   protected void resetScrolling()
   {
      cursor = new Cursor(this);
   }
   
   /**
    * Verify that this query is a scrolling query;  that is, that it has an
    * associated cursor.
    *
    * @throws  UnsupportedOperationException
    *          if a cursor has not been associated with this query.
    */
   protected void verifyScrolling()
   {
      if (!isScrolling())
      {
         throw new UnsupportedOperationException(
            "Cannot access cursor for a non-scrolling query");
      }
   }

   /**
    * Increment the counter of how many times the current result row of this
    * query has changed.
    */
   protected void incrementMoves()
   {
      moveCount++;
   }
   
   /**
    * Return an object which represents a property value which defines the
    * end of a <i>sort band</i>.  A <i>sort band</i> is a series of one or
    * more records which share the same value for a particular property,
    * where that property is used as the coarsest sorting criterion defined
    * by this query's <code>order by</code> clause.
    * <p>
    * If the break value for this query currently is non-<code>null</code>,
    * this indicates that the most recently retrieved record has crossed a
    * <i>sort band</i> boundary, and that a new band has begun.  This value
    * will only be non-<code>null</code> at the point at which such a record
    * is found, and will be <code>null</code> for all other records.
    * <p>
    * Note:  break values are only tracked for invocations of
    * <code>next</code> and <code>previous</code>.  Other retrievals will
    * results in break value being <code>null</code>.
    *
    * @return  Break value if dynamic query crossed a sort band boundary,
    *          else <code>null</code>.
    */
   protected abstract Object getBreakValue();

   /**
    * Reset the query to a clean state.
    */
   protected void reset()
   {
      // simulate the reset by closing and reopening the query
      this.close();
      this.open();
   }
}