Cursor.java
/*
** Module : Cursor.java
** Abstract : Cursor support for scrolling, dynamic queries
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description----------------------------------
** 001 ECF 20051215 @23737 Created initial version. Used by dynamic
** queries to manage scrolling and repositioning
** in a progressively updated results list.
** 002 ECF 20060216 @24717 Changed beforeFirst() method access. Made package
** private so CompoundQuery can access it.
** 003 ECF 20061019 @30526 Added reset() method. Resets cursor to a
** known, default state.
** 004 ECF 20061204 @31564 Integrated RepositionCache. This replaced a
** simple hash map approach which failed in
** cases where a lookup used an ID array which
** had fewer IDs than joined tables in a query.
** Also fixed reposition(int row) method variant
** to ensure cursor is always positioned before
** the target record.
** 005 ECF 20061206 @31585 Added boolean return value to reposition()
** variants which accept IDs. Indicates whether
** desired row was matched.
** 006 ECF 20070507 @33454 Fixed forward(). Set position to end of
** results after attempting to advance the
** cache.
** 007 ECF 20070518 @33679 Added contains() method. Tests whether the
** cursor's cache contains a particular primary
** key ID.
** 008 ECF 20071011 @35444 Fixed forward() and backward() methods. It
** was possible for the cursor's position to get
** out of sync with the associated query and for
** duplicate, ghost records to be cached by the
** cursor.
** 009 ECF 20080112 @36998 Removed restriction of dealing only with IDs.
** This class can now handle both DMO instances
** and primary key IDs when retrieving data.
** 010 ECF 20080731 @39317 Added rowid support. Refactored reposition*()
** methods.
** 011 ECF 20081106 @40321 Fixed regression introduced by H009 (@36998).
** Fixed contains() method to work whether IDs
** or DMOs are stored by the Cursor.
** 012 ECF 20090306 @42978 Added information to be tracked. An instance now
** knows if it has visited the first result in a set
** and which direction it has run off end of query
** results.
** 013 SVL 20140909 Modifications that allow cursor to be used as
** cache for dynamic scrolling queries.
** 014 SVL 20150216 Fixed positioning in scan mode during reposition.
** 015 SVL 20150331 deleteResultListEntry is now able to delete rows even if the cursor
** is positioned between them.
** 016 SVL 20150625 Fixed deletion of the first row.
** 017 SVL 20150722 Cursor position is set up properly before reposition scan.
** 018 OM 20150727 Granted package access to previous(), first() & last().
** 019 ECF 20160202 Replaced Apache commons logging with J2SE logging.
** 020 SVL 20160113 Fixed edge case of iterations past cached rows. Added referenceRow.
** 021 SVL 20160526 Added ability to set results.
** 022 SVL 20160608 Validation of reposition IDs is moved away from this class. Fixed
** reference row error for bulk delete. Added matching for null IDs.
** 023 SVL 20161007 Fix for the case when REPOSITION-TO-ROWID takes more ids than the
** actual query table count.
** 024 OM 20170209 Added return value for repositioning methods.
** 025 OM 20180926 Fixed repositionByID() to scan all available records.
** 026 SVL 20191002 Fixes for first/last. Added addResultAtCurrentPosition.
** 027 SVL 20191218 Fixed invalid result set generated by REPOSITION TO ROWID.
** SVL 20200409 Fixed reposition from row N to row N+1 using REPOSITION TO ROW N+1.
** 028 ECF 20200906 New ORM implementation.
** OM 20220202 Lowered access for reset(). Dropped unused import.
** SVL 20220320 Fill repo cache when a list of results is set.
** TW 20220831 Implemented toString for IDE debugging (refs: #6689)
** SVL 20220901 Fixed wrong entry being deleted from repoCache in deleteResultListEntry().
** 029 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 030 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 031 SB 20250325 Retrieve the primary key of the cached record instead of the record itself to
** account for the case when its state can become invalid. Refs #9402.
** 032 AL2 20250415 Allow consumers of repositionByID to decide if it should be a scan or not.
** 033 AL2 20250521 Repositioning to a row is done with NO_LOCK.
*/
/*
** 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 java.util.logging.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.LockType;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;
/**
* Implementation of a query results list scrollable cursor, for queries which
* fetch their results dynamically. This object maintains a transient list
* of results for a dynamic query, such that the user of the query may scroll
* forward, backward, to an absolute results list row, or to a result which
* corresponds with an array of primary key IDs (which represent a particular
* set of joined records). The list is considered transient because it may
* be discarded and rebuilt in certain cases where the user changes scrolling
* or query navigation direction.
* <p>
* This class is used by the {@link DynamicQuery} class and requires that the
* query it manages implement the {@link Scrollable} interface. It is not
* manipulated directly by business logic.
*/
final class Cursor
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(Cursor.class.getName());
/** Cached list of query results (as arrays of primary keys or DMOs) */
private List<Object[]> results = new ArrayList<>();
/**
* Last fetched row used as the reference row for future iterations. It is an array of record
* snapshots.
*/
private Record[] referenceRow = null;
/** Map of primary key lists of query results to their result list rows */
private final RepositionCache repoCache = new RepositionCache();
/** Scrollable query whose results are cursored */
private final Scrollable query;
/** Current position within the results list */
private double position = -0.5;
/** Flag indicating forward scroll direction */
private boolean forwardScroll = true;
/** Have we visited the first result in the result set? */
private boolean foundFirst = false;
/** Have we visited the full result set? */
private boolean fullSet = false;
/**
* Was the first row (or the last row if the scroll direction is backward) deleted by
* {@link #deleteResultListEntry}?
*/
private boolean firstRowDeleted = false;
/**
* Constructor which associates the cursor with the query it is managing.
*
* @param query
* Instance of a scrollable query.
*/
Cursor(Scrollable query)
{
this.query = query;
}
/**
* Reset cursor to a known default state.
*/
void reset()
{
results.clear();
repoCache.clear();
position = -0.5;
foundFirst = false;
forwardScroll = true;
fullSet = false;
firstRowDeleted = false;
referenceRow = null;
}
/**
* Set underlying cursor results.
*
* @param results
* Results to set: cached list of query results (as arrays of primary keys or DMOs).
*/
void setResults(List<Object[]> results)
{
reset();
this.results = results;
for (int i = 0; i < results.size(); i++)
{
addToRepoCache(results.get(i), i);
}
fullSet = true;
}
/**
* 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 scan
* <code>true</code> to scan all results in the event a match is
* not found in the cache; <code>false</code> to check the cache
* only.
* @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).
*
* @return <code>true</code> if a matching record was found and the
* cursor was repositioned to it; <code>false</code> if any ID
* was unknown or if a matching record could not be found (and we
* are in silent error mode).
*
* @throws ErrorConditionException
* if the specified ID represents the unknown value.
*/
boolean repositionByID(boolean scan, integer id)
{
Long[] serIDs = new Long[] { id.longValue() };
if (!validateReposition(id))
{
return false;
}
return reposition(serIDs, scan);
}
/**
* 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.
* <p>
* If the specified result does not already exist in the results list, the
* query is instructed to retrieve results sequentially, starting with the
* first result, caching each result in its list. This continues until
* either the desired result is found (and the cursor is then repositioned
* accordingly), or until no more results are available (in which case the
* cursor remains at its current position).
*
* @param scan
* <code>true</code> to scan all results in the event a match is
* not found in the cache; <code>false</code> to check the cache
* only.
* @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.
*
* @return <code>true</code> if a matching record was found and the
* cursor was repositioned to it; <code>false</code> if any ID
* was unknown or if a matching record could not be found (and we
* are in silent error mode).
*/
boolean repositionByID(boolean scan, rowid id1, rowid... joinIDs)
{
int len = joinIDs.length;
Long[] serIDs = new Long[len + 1];
serIDs[0] = id1.getValue();
for (int i = 0; i < len; i++)
{
rowid next = joinIDs[i];
serIDs[i + 1] = next.getValue();
}
return reposition(serIDs, scan);
}
/**
* 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.
* <p>
* If the specified result already exists in the results list, the cursor
* is immediately repositioned to just before that result. Otherwise,
* the behavior depends upon the value of the <code>scan</code> parameter.
* If it is <code>true</code>, the query is instructed to retrieve results
* sequentially, starting with the first available, caching each result in
* its list. This continues until either the desired result is found (in
* which case the cursor is repositioned accordingly), or until no more
* results are available (in which case the cursor remains at its current
* position).
*
* @param ids
* Array of primary key IDs, arranged from left to right to
* coincide with the records being joined by the underlying
* query.
* @param scan
* <code>true</code> to scan all results in the event a match is
* not found in the cache; <code>false</code> to check the cache
* only.
*
* @return <code>true</code> if a matching record was found and the
* cursor was repositioned to it; <code>false</code> if any ID
* was unknown or if a matching record could not be found (and we
* are in silent error mode).
*/
boolean reposition(Long[] ids, boolean scan)
{
// Check cache first.
ids = DBUtils.trimToTableCount(ids, query);
int row = repoCache.getRow(ids);
if (row >= 0)
{
position = forwardScroll ? row - 0.5 : row + 0.5;
return true;
}
if (!scan || isFullSet())
{
return false;
}
int size = results.size();
if (size > 0)
{
// Advance to just before last cached result.
position = size - 1.5;
// Synchronize query's results with cursor's position.
// This shouldn't lock the records because a reposition
// will end up in-between records.
if (forwardScroll)
{
query.next(LockType.NONE);
}
else
{
query.previous(LockType.NONE);
}
}
Object[] data;
while (true)
{
data = forwardScroll ? query.peekNext() : query.peekPrevious();
if (data == null)
break;
if (match(ids, data))
{
position = forwardScroll ? position - 0.5 : position + 0.5;
return true;
}
}
// Here we need to emulate a quirk/defect of Progress' reposition
// implementation: if no match was found, we leave the "current"
// position at the last checked position. Furthermore, if we are
// fetching on reposition, we have to load this last record into the
// associated buffer(s), to truly emulate the GET NEXT semantic.
if (query.isFetchOnReposition() && !results.isEmpty())
{
try
{
query.next(LockType.NONE);
}
catch (ErrorConditionException exc)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Error fetching record(s) during reposition", exc);
}
}
}
return false;
}
/**
* Get off-end state of query results.
*
* @return off-end state of query results.
*/
OffEnd getOffEnd()
{
if (!foundFirst)
{
return OffEnd.NONE;
}
if (fullSet && position > results.size() - 1)
{
return OffEnd.BACK;
}
if (position < 0)
{
return forwardScroll ? OffEnd.FRONT : OffEnd.BACK;
}
return OffEnd.NONE;
}
/**
* 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.
* <p>
* If the specified row does not already exist in the results list, the
* query is instructed to retrieve results sequentially, starting with the
* first result, caching each result in its list. This continues until
* either the desired row is reached (and the cursor is then repositioned
* accordingly), or until no more results are available (in which case the
* cursor remains at its current position).
* <p>
* Note that this method will clear the cache if the cursor previously had
* been scrolling backward.
*
* @param row
* 1-based index of the target position.
*
* @throws ErrorConditionException
* if <code>row</code> represents unknown value.
*/
void reposition(NumberType row)
{
if (validateReposition(row))
{
reposition(row.intValue());
}
}
/**
* 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.
* <p>
* If the specified row does not already exist in the results list, the
* query is instructed to retrieve results sequentially, starting with the
* first result, caching each result in its list. This continues until
* either the desired row is reached (and the cursor is then repositioned
* accordingly), or until no more results are available (in which case the
* cursor remains at its current position).
* <p>
* Note that this will clear the cache if we previously had been scrolling
* backward.
*
* @param row
* 1-based index of the target position.
*/
void reposition(int row)
{
// Adjust for 1-based index; disallow negative numbers.
row = Math.max(0, --row);
// Change to forward scroll mode if necessary.
if (!forwardScroll)
{
forwardScroll = true;
results.clear();
repoCache.clear();
referenceRow = null;
first();
query.first(LockType.NONE);
}
// Convert to relative move.
int jump = row - ((int) Math.ceil(position + 0.5));
if (jump < 0)
{
backward(-jump);
}
else if (jump > 0)
{
forward(jump);
}
else if (position < row)
{
// Using REPOSITION FORWARD we cannot jump from an on-record position to the next
// position, the minimal jump is 1.5 in this case. E.g. using REPOSITION FORWARD 1 from
// the position 5 we'll get to the position 6.5. This section handles this case.
position = Math.floor(position) + 0.5;
}
}
/**
* 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>
* <p>
* If the request carries the cursor beyond the end of the currently
* cached results list, the query is instructed to retrieve additional
* results sequentially. These are added to the results list. This
* continues until the request is satisfied, or until there are no more
* results available. In the latter case, the cursor is left positioned
* just beyond the last available result.
*
* @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 {@code rows} represents the unknown value.
*/
boolean forward(NumberType rows)
{
if (validateReposition(rows))
{
return forward(rows.intValue());
}
return false;
}
/**
* 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>
* <p>
* If the request carries the cursor beyond the end of the currently
* cached results list, the query is instructed to retrieve additional
* results sequentially. These are added to the results list. This
* continues until the request is satisfied, or until there are no more
* results available. In the latter case, the cursor is left positioned
* just beyond the last available result.
*
* @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.
*/
boolean forward(int rows)
{
double expectedPosition = position + rows;
if (forwardScroll)
{
int cacheSize = results.size();
double gap = Math.floor(position) + 1 + rows - cacheSize;
if (gap >= 0)
{
if (!fullSet)
{
if (cacheSize > 0)
{
// Advance to just before last cached result.
position = cacheSize - 1.5;
// Synchronize query's results with cursor's position.
// Don't enforce locking on the record; otherwise the reposition
// will pend for other locks.
query.next(LockType.NONE);
// If gap == 0 then we are on the last cached row and we should sync query
// because next() can be called from this position.
}
for (int i = 0; i < gap; i++)
{
Object[] row = query.peekNext();
if (row == null)
{
break;
}
}
}
position = results.size() - 1;
}
else
{
position += rows;
}
// Ensure cursor is between two entries.
position = Math.floor(position) + 0.5;
}
else
{
position -= rows;
// Ensure cursor is between two entries.
position = Math.ceil(position) - 0.5;
if (position < -0.5)
{
position = -0.5;
}
}
return Math.abs(expectedPosition - position) < 1;
}
/**
* 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>
* <p>
* If the request carries the cursor before the start of the currently
* cached results list, the query is instructed to retrieve additional,
* previous results sequentially. These are prepended to the results list.
* This continues until the request is satisfied, or until there are no
* more results available. In the latter case, the cursor is left
* positioned just before the first available result.
*
* @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 {@code rows} represents the unknown value.
*/
boolean backward(NumberType rows)
{
if (validateReposition(rows))
{
return backward(rows.intValue());
}
return false;
}
/**
* 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>
* <p>
* If the request carries the cursor before the start of the currently
* cached results list, the query is instructed to retrieve additional,
* previous results sequentially. These are prepended to the results list.
* This continues until the request is satisfied, or until there are no
* more results available. In the latter case, the cursor is left
* positioned just before the first available result.
*
* @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.
*/
boolean backward(int rows)
{
double expectedPosition = position - rows;
if (forwardScroll)
{
position -= rows;
// Ensure cursor is between two entries.
position = Math.floor(position) + 0.5;
if (position < -0.5)
{
position = -0.5;
}
}
else
{
int cacheSize = results.size();
double gap = Math.floor(position) + rows - cacheSize;
if (gap > 0)
{
if (!fullSet)
{
if (cacheSize > 0)
{
// Advance to just before first cached result.
position = cacheSize - 1.5;
// Synchronize query's results with cursor's position.
query.previous(LockType.NONE);
}
for (int i = 0; i < gap; i++)
{
Object[] row = query.peekPrevious();
if (row == null)
{
break;
}
}
}
position = forwardScroll ? -0.5 : results.size() - 0.5;
}
else
{
position += rows;
}
// Ensure cursor is between two entries.
position = forwardScroll ? Math.floor(position) + 0.5 : Math.ceil(position) - 0.5;
}
return Math.abs(expectedPosition - position) < 1;
}
/**
* 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 associated
* query.
*
* @return Current results list size.
*/
integer size()
{
return new integer(results.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.
*/
integer currentRow()
{
if (!foundFirst || !forwardScroll || results.size() == 0)
{
return (new integer());
}
// Determine row at which or before which we are positioned, adjusting
// for 0-based index.
int row = (int) Math.ceil(position) + 1;
return (new integer(row));
}
/**
* Indicate whether the cursor has crossed the specified boundary (front or
* back) of the associated query's list of available results. If
* <code>true</code>, this indicates that the query cannot produce any more
* results in that scroll direction. If <code>boundary</code> is
* <code>null</code> or <code>OffEnd.NONE</code>, assume the caller just
* wants to know if the cursor has gone off-end at all (either direction).
*
* @param boundary
* One of the <code>OffEnd</code> enum types, or <code>null</code>
* to indicate we want to know if the cursor has gone off either
* end of the result list (equivalent to specifying the enum type
* <code>NONE</code>).
*
* @return <code>true</code> if the cursor is off the specified end of its
* query's results list, else <code>false</code>.
*/
boolean isOffEnd(OffEnd boundary)
{
OffEnd offEnd = getOffEnd();
if (boundary == null || OffEnd.NONE.equals(boundary))
{
return !OffEnd.NONE.equals(offEnd);
}
return offEnd.equals(boundary);
}
/**
* Store a result in the first position of the cached results list. If
* <code>ids</code> is <code>null</code>, this indicates the query has run
* off the beginning of its available results.
*
* @param data
* Result to add: the primary keys of the records backing the
* result to be added or the records themselves (or a mix).
*/
void addResultFirst(Object[] data)
{
referenceRow = null;
foundFirst = true;
// Change to forward scroll mode if necessary.
if (!forwardScroll)
{
forwardScroll = true;
results.clear();
repoCache.clear();
}
if (data == null)
{
// empty data set
fullSet = true;
position = -0.5;
return;
}
firstRowDeleted = false;
results.add(0, data);
first();
addToRepoCache(data);
}
/**
* Store a result in the last position of the cached results list. If
* <code>ids</code> is <code>null</code>, this indicates the query has run
* off the end of its available results.
*
* @param data
* Result to add: the primary keys of the records backing the
* result to be added or the records themselves (or a mix).
*/
void addResultLast(Object[] data)
{
referenceRow = null;
foundFirst = true;
// Change to backward scroll mode if necessary.
if (forwardScroll)
{
forwardScroll = false;
results.clear();
repoCache.clear();
}
if (data == null)
{
// empty data set
fullSet = true;
position = -0.5;
return;
}
firstRowDeleted = false;
results.add(data);
last();
addToRepoCache(data);
}
/**
* Add a result to the current position of the cached results list.
*
* @param data
* Result to add: the primary keys of the records backing the
* result to be added or the records themselves (or a mix).
*/
void addResultAtCurrentPosition(Object[] data)
{
if (position < 0)
{
position = 0;
}
else if (position > results.size() - 1)
{
position = results.size();
}
else
{
position = (int) Math.floor(position) + 1;
}
if (results.isEmpty())
{
foundFirst = true;
}
results.add((int) position, data);
addToRepoCache(data);
}
/**
* Add a result to the end of the cached results list. If <code>ids</code>
* is <code>null</code>, this indicates the query has run off the end of
* its available results.
*
* @param data
* Result to add: the primary keys of the records backing the
* result to be added or the records themselves (or a mix).
*/
void addResultNext(Object[] data)
{
if (query.getTableCount() == 1)
{
referenceRow = null;
}
if (data == null)
{
if (!foundFirst)
{
foundFirst = true;
fullSet = true;
position = -0.5;
return;
}
if (forwardScroll)
{
fullSet = true;
position = results.size() - 0.5;
}
return;
}
if (results.isEmpty())
{
addResultFirst(data);
return;
}
// Add result to end of list; set current position to end of results list.
results.add(data);
position = results.size() - 1;
addToRepoCache(data);
}
/**
* Prepend a result to the beginning of the cached results list. If
* <code>ids</code> is <code>null</code>, this indicates the query has run
* off the beginning of its available results.
*
* @param data
* Result to add: the primary keys of the records backing the
* result to be added or the records themselves (or a mix).
*/
void addResultPrevious(Object[] data)
{
if (query.getTableCount() == 1)
{
referenceRow = null;
}
if (data == null)
{
if (!foundFirst)
{
foundFirst = true;
fullSet = true;
position = -0.5;
return;
}
if (!forwardScroll)
{
fullSet = true;
position = -0.5;
forwardScroll = true;
Collections.reverse(results);
}
return;
}
if (results.isEmpty())
{
addResultLast(data);
return;
}
results.add(data);
position = results.size() - 1;
addToRepoCache(data);
}
/**
* Store the last fetched row as the reference row for future iterations.
*
* @param event
* Event caused by update of a record of specific type.
*/
void storeReferenceRow(RecordChangeEvent event)
{
if (fullSet || results.size() == 0 ||
(query.getTableCount() == 1 && referenceRow != null) ||
(event != null && event.isFullBulkDelete()))
{
return;
}
RecordBuffer[] buffers;
// get all record buffers
if (query instanceof CompoundQuery)
{
CompoundQuery q = (CompoundQuery) query;
buffers = new RecordBuffer[query.getTableCount()];
int i = 0;
Iterator<RecordBuffer> iter = q.recordBuffers();
while (iter.hasNext())
{
RecordBuffer buffer = iter.next();
buffers[i++] = buffer;
}
}
else
{
buffers = query.getRecordBuffers();
}
// get last fetched row
Object[] rowData = results.get(results.size() - 1);
if (referenceRow == null)
{
referenceRow = new Record[buffers.length];
}
for (int i = 0; i < buffers.length; i++)
{
if (referenceRow[i] != null)
{
continue;
}
RecordBuffer buffer = buffers[i];
Object rowElement = rowData[i];
Long rowElementId = rowElement instanceof Record
? ((Record) rowElement).primaryKey()
: (Long) rowElement;
Record dmo = null;
// check event if it contains target DMO
if (event != null &&
event.getSource().getDMOInterface().equals(buffer.getDMOInterface()))
{
Record snapshot = event.getSnapshot();
if (snapshot != null && snapshot.primaryKey().equals(rowElementId))
{
referenceRow[i] = snapshot.snapshot();
dmo = snapshot;
}
}
// directly load DMO
if (dmo == null && rowElementId != null)
{
Persistence persistence = buffer.getPersistence();
Session session = persistence.getSession(!buffer.getDmoInfo().multiTenant);
try
{
dmo = session.get(buffer.getDMOImplementationClass(), rowElementId);
}
catch (PersistenceException exc)
{
throw new IllegalArgumentException(exc);
}
if (dmo != null)
{
referenceRow[i] = dmo.snapshot();
try
{
buffer.evictDMOIfUnused(dmo);
}
catch (PersistenceException exc)
{
DBUtils.handleException(buffer.getDatabase(), exc);
ErrorManager.recordOrThrowError(exc);
}
}
}
if (dmo == null)
{
// fallback
referenceRow = null;
break;
}
}
}
/**
* Reset reference row snapshots starting from the given buffer index.
*
* @param startIndex
* Starting buffer index in the set of all buffers participating in the query.
*/
void resetReferenceRow(int startIndex)
{
if (startIndex == 0)
{
referenceRow = null;
}
else if (referenceRow != null)
{
Arrays.fill(referenceRow, startIndex, referenceRow.length, null);
}
}
/**
* Produce a string representation of the cursor.
*
* @return String representation of the cursor.
*/
public String toString()
{
StringBuilder buf = new StringBuilder();
buf.append(super.toString())
.append("\n").append(" position:").append(position).append(" (0-based)")
.append("\n").append(" fullSet:").append(fullSet)
.append("\n").append("repoCache:").append(repoCache.toString().replace("\n", "\n "));
return buf.toString();
}
/**
* Get the reference row for future iterations.
*
* @return array of record snapshots which form the reference row for future iterations.
*/
public Record[] getReferenceRow()
{
return referenceRow;
}
/**
* Move the cursor exactly onto the index of the first result <i>in the
* cache</i>, and return the primary key array representing that result.
* If there is no such result, the original cursor position is restored.
* Note that such a condition does not necessarily imply that no further
* result is available, only that none was cached by the cursor.
*
* @return Primary key array representing the first cached result row, or
* <code>null</code> if no such result was cached.
*/
Object[] getFirst()
{
if (results.isEmpty() || (!forwardScroll && !fullSet))
{
return null;
}
first();
return getResult(-0.5);
}
/**
* Move the cursor exactly onto the index of the last result <i>in the
* cache</i>, and return the primary key array representing that result.
* If there is no such result, the original cursor position is restored.
* Note that such a condition does not necessarily imply that no further
* result is available, only that none was cached by the cursor.
*
* @return Primary key array representing the last cached result row, or
* <code>null</code> if no such result was cached.
*/
Object[] getLast()
{
if (results.isEmpty() || (!fullSet && forwardScroll))
{
return null;
}
last();
return getResult(position + 0.5);
}
/**
* Move the cursor exactly onto the index of the next result <i>in the
* cache</i>, and return the primary key array representing that result.
* If there is no such result, the original cursor position is restored.
* Note that such a condition does not necessarily imply that no further
* result is available, only that none was cached by the cursor.
*
* @return Primary key array representing the next cached result row, or
* <code>null</code> if no such result was cached.
*/
Object[] getNext()
{
if (results.isEmpty())
{
return null;
}
next();
return getResult(forwardScroll ? position - 0.5 : position + 0.5);
}
/**
* Move the cursor exactly onto the index of the previous result <i>in the
* cache</i>, and return the primary key array representing that result.
* If there is no such result, the original cursor position is restored.
* Note that such a condition does not necessarily imply that no further
* result is available, only that none was cached by the cursor.
*
* @return Primary key array representing the previous cached result row,
* or <code>null</code> if no such result was cached.
*/
Object[] getPrevious()
{
if (results.isEmpty())
{
return null;
}
previous();
return getResult(forwardScroll ? position + 0.5 : position - 0.5);
}
/**
* Retrieve the virtual, composite row of results at the cursor's current
* position. If there are no results cached, return null.
*
* @return Array of results for the current row.
*/
Object[] getRow()
{
if (results.isEmpty())
{
return null;
}
return getResult(position);
}
/**
* Get last loaded result row.
*
* @return last loaded result row
*/
Object[] peekLastLoadedResult()
{
int size = results.size();
return size > 0 ? results.get(size - 1) : null;
}
/**
* Indicate whether the results cache contains the given ID within any of
* its arrays. Each array within the cache is checked at the given index
* for a match.
*
* @param id
* ID to match.
* @param index
* Index position at which a match is attempted within each array
* stored in the results cache.
*
* @return <code>true</code> if the cache contains the given ID, else
* <code>false</code>.
*/
boolean contains(Long id, int index)
{
for (Object[] next : results)
{
Object o = next[index];
Long nextID = null;
if (o instanceof Record)
{
nextID = ((Record) o).primaryKey();
}
else
{
nextID = (Long) o;
}
if (nextID.equals(id))
{
return true;
}
}
return false;
}
/**
* Move the cursor to just before the index of the first entry, whether or
* not it actually exists in the results list.
*/
void beforeFirst()
{
position = -0.5;
}
/**
* Determines if the query has fetched all of its results. I.e. if it has reached the last
* result starting from the first one, of the first stating from the last one.
*
* @return <code>true</code> if the query has fetched all of its results.
*/
boolean isFullSet()
{
return fullSet;
}
/**
* Determines if the cache is build in the forward direction (starting from the first result),
* or backward direction (starting from the last result).
*
* @return <code>true</code> if the cache is build in the forward direction, <code>false</code>
* if it is build in the backward direction
*/
boolean isForwardScroll()
{
return forwardScroll;
}
/**
* Determines if we have visited the first result in the result set.
*
* @return <code>true</code> if we have visited the first result in the result set.
*/
boolean isFoundFirst()
{
return foundFirst;
}
/**
* Deletes the current row in the cache.
*
* @param allowBetweenRows
* If <code>true</code> then the cursor can be positioned between rows (the next
* row will be deleted). <code>false</code> for conventional DELETE-RESULT-LIST-ENTRY()
* mode where the cursor should be positioned on a row (otherwise <code>false</code> is
* returned).
*
* @return <code>true</code> on success.
*/
boolean deleteResultListEntry(boolean allowBetweenRows)
{
boolean betweenRows = Math.floor(position) != position;
OffEnd offEnd = getOffEnd();
if ((OffEnd.NONE.equals(offEnd) ||
(allowBetweenRows && OffEnd.FRONT.equals(offEnd))) &&
results.size() > 0 &&
(allowBetweenRows || !betweenRows))
{
storeReferenceRow(null);
if (position == 0)
{
firstRowDeleted = true;
}
if (allowBetweenRows && betweenRows)
{
int targetPosition = (int) (position + 0.5);
removeFromRepoCache(results.remove(targetPosition));
}
else
{
removeFromRepoCache(getRow());
results.remove((int) position);
position -= 0.5;
}
return true;
}
return false;
}
/**
* Move the cursor exactly onto the index of the next entry, whether or not it actually exists
* in the results list.
*/
void next()
{
if (forwardScroll)
{
position = Math.floor(position) + 1;
}
else
{
position = Math.ceil(position) - 1;
}
}
/**
* Move the cursor exactly onto the index of the first entry, whether or not it actually exists
* in the results list.
*/
void first()
{
position = 0;
}
/**
* Move the cursor exactly onto the index of the last entry, whether or not it actually exists
* in the results list.
*/
void last()
{
if (forwardScroll)
{
position = Math.max(0, results.size() - 1);
}
else
{
position = 0;
}
}
/**
* Move the cursor exactly onto the index of the previous entry, whether or not it actually
* exists in the results list.
*/
void previous()
{
if (forwardScroll)
{
position = Math.ceil(position) - 1;
}
else
{
position = Math.floor(position) + 1;
}
}
/**
* Determines if the first row (or the last row if the scroll direction is backward) deleted by
* {@link #deleteResultListEntry}.
*
* @return See above.
*/
boolean isFirstRowDeleted()
{
return firstRowDeleted;
}
/**
* Retrieve the result at the cursor's current position, which should
* always be at an index boundary (not between result indices). If there
* is no such result, the cursor's current position is reset to the
* position provided and <code>null</code> is returned. The latter
* condition indicates that the cursor has been moved beyond a cached
* result, such that the caller needs to attempt to query a result and
* cache it.
*
* @param resetPosition
* Position to which the cursor must be reset in the event no
* result is available in the cached results list at the cursor's
* current position.
*
* @return The data at the cursor's current position, or <code>null</code>
* if no such data exists. For deleted DMOs their record ID will
* be retrieved instead.
*/
private Object[] getResult(double resetPosition)
{
// It is safe to truncate here, since position is expected to be set to
// an integer boundary when this method is called.
int index = (int) position;
if (index < 0 || index >= results.size())
{
position = resetPosition;
return null;
}
Object[] data = results.get(index);
for (int i = 0; i < data.length; i++)
{
Object obj = data[i];
if (obj instanceof Record && ((Record) obj).checkState(DmoState.DELETED))
{
// If the cached DMO instance has been deleted put its record ID instead to force
// a refetch of the record, as its state could be updated. See #9402 for an example.
data[i] = ((Record) obj).primaryKey();
}
}
return data;
}
/**
* Associate an array of primary key IDs representing a query result with
* the cursor's current position. This is used to enable a fast look up
* when performing a "reverse" reposition; that is, a reposition where
* the cursor is given an array of primary keys and must move to the
* associated results list row.
* <p>
* Note that this method should only be invoked when the current cursor
* position corresponds with the specified result, otherwise the row will
* be mismatched for the result.
*
* @param data
* Array of primary keys or DMOs representing a query result.
*/
private void addToRepoCache(Object[] data)
{
int row = (int) Math.ceil(position);
addToRepoCache(data, row);
}
/**
* Associate an array of primary key IDs representing a query result with
* the specified cursor position. This is used to enable a fast look up
* when performing a "reverse" reposition; that is, a reposition where
* the cursor is given an array of primary keys and must move to the
* associated results list row.
*
* @param data
* Array of primary keys or DMOs representing a query result.
* @param row
* 0-based row number associated with the specified query result.
*/
private void addToRepoCache(Object[] data, int row)
{
int len = data.length;
Long[] ids = new Long[len];
for (int i = 0; i < len; i++)
{
Object datum = data[i];
ids[i] = (datum instanceof Record ? ((Record) datum).primaryKey() : (Long) datum);
}
repoCache.add(ids, row);
}
/**
* Delete the cache entry associated with the specified array of primary keys or DMOs .
*
* @param data
* Array of primary keys or DMOs associated with the entry to be deleted.
*/
private void removeFromRepoCache(Object[] data)
{
int len = data.length;
Long[] ids = new Long[len];
for (int i = 0; i < len; i++)
{
Object datum = data[i];
ids[i] = (datum instanceof Record ? ((Record) datum).primaryKey() : (Long) datum);
}
repoCache.remove(ids);
}
/**
* Detect whether all elements in one object array match those at the same
* positions within another array. The length of the first array
* determines the number of elements checked. That is, if the second
* array contains more elements than the first, excess elements in the
* second array are ignored.
*
* @param a1
* First array. May have fewer or the same number of elements as
* <code>a2</code>.
* @param a2
* Second array. May have the same number or more elements than
* <code>a1</code>.
*
* @return <code>true</code> if all elements in <code>a1</code> equal
* those elements at corresponding positions in <code>a2</code>.
*/
private boolean match(Long[] a1, Object[] a2)
{
int len = a1.length;
for (int i = 0; i < len; i++)
{
Long id = a1[i];
Object datum = a2[i];
if (datum instanceof Record)
{
datum = ((Record) datum).primaryKey();
}
if (id == null)
{
if (datum != null)
{
return false;
}
}
else if (!id.equals(datum))
{
return false;
}
}
return true;
}
/**
* Validate a numeric value provided to a reposition request to ensure
* that it does not represent the unknown value.
*
* @param value
* ID or row number/offset to be validated.
*
* @return <code>true</code> if the number is valid; <code>false</code>
* if it is invalid, but we are in silent error mode.
*
* @throws ErrorConditionException
* if the number is invalid and we are not in silent error mode.
*/
private boolean validateReposition(BaseDataType value)
{
if (value.isUnknown())
{
ErrorManager.recordOrThrowError(
3165,
"Could not evaluate reposition amount for query");
return false;
}
return true;
}
}