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