# 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();
       }
    }