Presorter.java
/*
** Module : Presorter.java
** Abstract : Helper class which performs client-side sorting
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 ECF 20080112 @36995 Created initial version. Helper class which
** performs client-side sorting.
** 002 CA 20080529 @38435 Unknown values must be sorted using a high
** order. Ref: #38432
** 003 ECF 20080530 @38482 Refactored SortHelper inner class. Now uses
** DataSorter to perform comparisons.
** 004 ECF 20090416 @41807 Added test for thread interruption during
** retrieve loop in SortedResults c'tor. For
** large result sets, this loop could be long
** running, so it needs to honor stop requests.
** 005 ECF 20090619 @42770 Minor memory and performance optimizations.
** 006 SVL 20090723 @43347 IncrementalResults class was added.
** 007 SVL 20090730 @43461 Added session closing handling into
** IncrementalResults.
** 008 ECF 20090810 @43589 Changed to accommodate SessionListener API
** change. sessionClosing() method was replaced with
** sessionEvent().
** 009 ECF 20150327 Throw NPE from addSortCriterion to detect null resolvables earlier
** (i.e., before we attempt to resolve them during results sorting).
** 010 ECF 20150801 Minor simplification of code.
** 011 ECF 20160227 Use FieldReference.property() instead of propertyName(), which was
** removed.
** 012 ECF 20171128 Allow for outer joins, which can cause null DMOs in results.
** 013 OM 20180515 Added dynamic sorting of the result set.
** 014 OM 20180901 Improved dynamic sorting.
** 015 ECF 20181106 Added runtime support for {FIRST|LAST}-OF methods.
** 016 ECF 20190427 Fixed results caching.
** 017 ECF 20200906 New ORM implementation.
** 018 CA 20210923 Fixed SortedResults, when the component retrieves only the DMO id.
** 019 DDF 20240207 Only cache the results once when TRANSACTION_COMMITTING event is notified.
** Removed unnecessary import.
** DDF 20240209 Do not cache previously cached results.
** 020 AL2 20240906 Implemented get(forceOnlyId).
** 021 LS 20241101 Added a flag to createSortedResults() and SortedResults constructor
** that indicates if the records should be loaded into their buffers.
** 022 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 023 ICP 20250129 Used logical constants to leverage cached values.
*/
/*
** 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.function.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.persist.event.SessionListener;
/**
* This helper class performs client-side (i.e., relative to the database
* server) sorting of a result set on behalf of a query. One or more
* instances of <code>Resolvable</code> provided by business application code
* are used to sort the result set returned by the database. This class
* supports the Progress semantics of "client-side" results sorting and break
* groups. Multiple levels of nested break groups are supported.
* <p>
* Sorting criteria is specified using the <code>addSortCriterion</code>
* methods. The order in which criteria are added is significant; the first
* criterion added dictates the coarsest level of sorting. Each criterion
* added subsequently only sorts rows within that criterion added immediately
* previous to it.
* <p>
* The sort operation is performed once, and it is up to the owning query to
* cache these results for later record retrieval requests.
*/
final class Presorter
{
/** Reference to the owning query object */
private final Presortable query;
/** Map of break group keys to <code>SortHelper</code> values */
private final Map<Resolvable, SortHelper> sortHelpers = new LinkedHashMap<>(4);
/** The original set of sort helpers. */
private Map<Resolvable, SortHelper> originalSortHelpers = null;
/** Are sort criteria used as break groups? */
private boolean breakGroups = false;
/**
* Constructor which stores a reference to the owning query object. This
* reference is used to extract necessary information during and after the
* sort operation.
*
* @param query
* Owning query object.
*/
Presorter(Presortable query)
{
this.query = query;
}
/**
* Get the break group keys.
*
* @return a <code>Set</code> containing the break group keys.
*/
public Set<Resolvable> getSortKeys()
{
return sortHelpers.keySet();
}
/**
* Determines whether the sort criteria added to this <code>Presorter</code> match the leading
* components of the given criteria.
*
* @param criteria
* Criteria to be compared with the sort criteria added to this
* <code>Presorter</code>.
*
* @return See above.
*/
public boolean sortCriteriaMatchSortPhrase(List<SortCriterion> criteria)
{
if (criteria.size() < sortHelpers.size())
{
return false;
}
int i = 0;
for (Map.Entry<Resolvable, SortHelper> entry : sortHelpers.entrySet())
{
Resolvable resolvable = entry.getKey();
if (!(resolvable instanceof FieldReference))
{
return false;
}
FieldReference fieldReference = (FieldReference) resolvable;
SortHelper helper = entry.getValue();
SortCriterion sortCriterion = criteria.get(i);
String propertyName1 = fieldReference.getProperty();
String propertyName2 = sortCriterion.getUnqualifiedName();
boolean ascending1 = (helper.getSorter().getDirection() > 0);
boolean ascending2 = sortCriterion.isAscending();
if (!propertyName1.equals(propertyName2) ||
(ascending1 != ascending2))
{
return false;
}
i++;
}
return true;
}
/**
* Add a single sort criterion, in the form of a <code>Resolvable</code>
* object which is resolved at sort time. It is the value of the resolved
* result which is used for the sort.
* <p>
* The sort is ascending, from lowest resolved value to highest.
*
* @param sort
* A resolvable object whose resolved result is used for sorting.
*/
void addSortCriterion(Resolvable sort)
{
addSortCriterion(sort, false);
}
/**
* Add a single sort criterion, in the form of a <code>Resolvable</code> object which is
* resolved at runtime. It is the value of the resolved result which is used for the sort.
*
* @param sort
* A resolvable object whose resolved result is used for sorting. May not be
* <code>null</code>.
* @param descending
* if <code>true</code>, the sort is descending, from highest
* resolved value to lowest; otherwise, the sort is ascending.
*
* @throws NullPointerException
* if <code>sort</code> is <code>null</code>.
*/
void addSortCriterion(Resolvable sort, boolean descending)
{
if (sort == null)
{
throw new NullPointerException("Resolvable sort object may not be null");
}
sortHelpers.put(sort, new SortHelper(!descending));
}
/**
* Add a single sort criterion, in the form of a {@code Resolvable} object which is
* resolved at runtime. It is the value of the resolved result which is used for the sort.
* <p>
* Unlike {@link #addSortCriterion} methods, the criterion is added with higher priority.
*
* @param sort
* A resolvable object whose resolved result is used for sorting. May not be
* {@code null}.
* @param ascending
* if {@code true}, the sort is ascending, from lowest resolved value to highest;
* otherwise, the sort is descending.
*
* @throws NullPointerException
* if {@code sort} is {@code null}.
*/
void addDynamicSortCriterion(Resolvable sort, boolean ascending)
{
if (sort == null)
{
throw new NullPointerException("Resolvable sort object may not be null");
}
if (originalSortHelpers == null)
{
// save a copy of the original [sortHelpers] before adding the first sort criterion
originalSortHelpers = new HashMap<>();
originalSortHelpers.putAll(sortHelpers);
}
// put in front, with higher priority
Map<Resolvable, SortHelper> newSorters = new LinkedHashMap<>();
newSorters.put(sort, new SortHelper(ascending));
// remove old sort direction of this criterion, avoiding overwriting with old direction
sortHelpers.remove(sort);
newSorters.putAll(sortHelpers);
sortHelpers.clear();
sortHelpers.putAll(newSorters);
}
/**
* Drop all current sort criteria added dynamically.
*/
void clearDynamicSortCriteria()
{
if (originalSortHelpers == null)
{
return; // no-op
}
// restore the original sort configuration
sortHelpers.clear();
sortHelpers.putAll(originalSortHelpers);
}
/**
* Enable all sort criteria set for this query to act as break group
* categories. When the query's results are presorted, the contents of
* each break group are determined.
*/
void enableBreakGroups()
{
breakGroups = true;
}
/**
* Determines whether sort criteria set for this query acts as break group
* categories.
*
* @return See above.
*/
boolean isBreakGroupsEnabled()
{
return breakGroups;
}
/**
* Determine whether the query result row currently being visited is the
* first row in the presorted results list.
*
* @return <code>true</code> if the current result row is the first row,
* else <code>false</code>.
*/
logical isFirst()
{
return (new logical(query.getSortedResults().isFirst()));
}
/**
* Determine whether the query result row currently being visited is the
* first row within a break group whose category is identified by the
* specified resolvable object.
*
* @param key
* Break group category key. Must be the same object reference
* as was specified previously when invoking one of the
* <code>addSortCriterion</code> method variants.
*
* @return <code>true</code> if break groups are enabled and current row
* is first in the specified break group;
* <code>false</code> if break groups have not been enabled or
* current row is not first in the specified break group.
*/
logical isFirstOfGroup(Resolvable key)
{
if (!breakGroups)
{
return logical.FALSE;
}
return logical.of(isNewBreakGroup(key));
}
/**
* Determine whether the query result row currently being visited is the first row of the
* query's result set or the first row within a break group whose category is identified by
* the specified BREAK-BY level.
*
* @param level
* Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
* criterion, etc. Must not be negative or greater than the number of sort criteria.
*v
* @return {@code true} if the current row is first in the result set or within the specified
* break group; else {@code false}.
*/
logical isFirstOf(integer level)
{
return alignsWithBreakGroup(level, this::isFirst, this::isFirstOfGroup);
}
/**
* Determine whether the query result row currently being visited is the last row of the
* query's result set or the last row within a break group whose category is identified by
* the specified BREAK-BY level.
*
* @param level
* Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
* criterion, etc. Must not be negative or greater than the number of sort criteria.
*
* @return {@code true} if the current row is last in the result set or within the specified
* break group; else {@code false}.
*/
logical isLastOf(integer level)
{
return alignsWithBreakGroup(level, this::isLast, this::isLastOfGroup);
}
/**
* Determine whether the query result row currently being visited is the
* last row in the presorted results list.
*
* @return <code>true</code> if the current result row is the last row,
* else <code>false</code>.
*/
logical isLast()
{
return (new logical(query.getSortedResults().isLast()));
}
/**
* Determine whether the query result row currently being visited is the
* last row within a break group whose category is identified by the
* specified resolvable object.
*
* @param key
* Break group category key. Must be the same object reference
* as was specified previously when invoking one of the
* <code>addSortCriterion</code> method variants.
*
* @return <code>true</code> if break groups are enabled and current row
* is last in the specified break group;
* <code>false</code> if break groups have not been enabled or
* current row is not last in the specified break group.
*/
logical isLastOfGroup(Resolvable key)
{
return (new logical(isLastOfBreakGroup(key)));
}
/**
* Determine whether the query result row currently being visited is the
* last row within a break group whose category is identified by the
* specified resolvable object.
*
* @param key
* Break group category key. Must be the same object reference
* as was specified previously when invoking one of the
* <code>addSortCriterion</code> method variants.
*
* @return <code>true</code> if break groups are enabled and current row
* is last in the specified break group;
* <code>false</code> if break groups have not been enabled or
* current row is not last in the specified break group.
*/
boolean isLastOfBreakGroup(Resolvable key)
{
if (!breakGroups || key == null)
{
return false;
}
Results results = query.getSortedResults();
int row = results.getRowNumber();
return (results.isLast() || getSortHelper(key).isLastOfGroup(row));
}
/**
* Determine whether the query result row currently being visited is the
* first row within a break group whose category is identified by the
* specified resolvable object.
* <p>
* This is the backing worker method for {@link #isFirstOfGroup} and is
* used internally by the query to manage accumulators.
*
* @param key
* Break group category key. Must be the same object reference
* as was specified previously when invoking one of the
* <code>addSortCriterion</code> method variants.
*
* @return <code>true</code> if current row is first in a break group;
* <code>false</code> if it is not; unknown value if break groups
* have not been enabled.
*/
boolean isNewBreakGroup(Resolvable key)
{
if (!breakGroups)
{
return false;
}
int row = query.getSortedResults().getRowNumber();
SortHelper helper = getSortHelper(key);
return helper.isFirstOfGroup(row);
}
/**
* Create a result set that is sorted according to the sort criteria
* provided to this object. Unsorted results are extracted from the
* <code>unsorted</code> parameter. They are sorted and stored into
* another <code>Results</code> object, which is returned.
*
* @param unsorted
* A set of unsorted, preselect results.
* @param loadDMOs
* A flag that indicates if the records
* should be loaded into their associated record buffers.
*
* @return The sorted results retrieved from <code>unsorted</code>.
*
* @throws PersistenceException
*/
Results createSortedResults(Results unsorted, boolean loadDMOs)
throws PersistenceException
{
return (new SortedResults(unsorted, loadDMOs));
}
/**
* Create a result set that will interrogate <code>sorted</code> results
* iteratively on request (not up front, as results set returned by
* {@link #createSortedResults} does). Note that passed results should
* already be in a proper order.
*
* @param sorted
* A set of sorted, preselect results.
*
* @return Iterative results set for the <code>sorted</code> results.
*
* @throws PersistenceException
*/
Results createIncrementalResults(Results sorted)
throws PersistenceException
{
return (new IncrementalResults(sorted));
}
/**
* Get the sort helper, if any, associated with the given break group key.
*
* @param key
* Break group category key.
*
* @return Sort helper object associated with the target break group
* category.
*
* @throws IllegalArgumentException
* if the break group key is invalid.
*/
private SortHelper getSortHelper(Resolvable key)
{
SortHelper helper = sortHelpers.get(key);
if (helper == null)
{
throw new IllegalArgumentException("Invalid break group key");
}
return helper;
}
/**
* Determine whether the query result row currently being visited aligns with the first or
* last row of the query's result set or within a break group whose category is identified by
* the specified BREAK-BY level. The nature of the alignment (first or last) is determined by
* the lambda expressions passed into this method.
*
* @param level
* Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
* criterion, etc. Must not be negative or greater than the number of sort criteria.
* @param wholeFn
* Lambda expression to be used to determine alignment with the whole query's result
* set.
* @param groupFn
* Lambda expression to be used to determine alignment with a particular break group.
*
* @return {@code true} if the current row aligns with the results as specified above;
* else {@code false}.
*/
private logical alignsWithBreakGroup(integer level,
Supplier<logical> wholeFn,
Function<Resolvable, logical> groupFn)
{
int lvl = level.intValue();
if (level.isUnknown() || lvl < 0 || lvl > sortHelpers.size())
{
String msg = "First argument to FIRST-OF or LAST-OF must be the index of which " +
"BREAK-BY phrase is relevant.";
ErrorManager.recordOrThrowError(14294, msg);
return (new logical(false));
}
// level 0 indicates whole query, not a specific break-by group
if (lvl == 0)
{
return wholeFn.get();
}
if (breakGroups)
{
// sortHelpers iterates in the order break-by groups were added
int i = 1;
for (Resolvable key : sortHelpers.keySet())
{
if (i++ == lvl)
{
return groupFn.apply(key);
}
}
}
return (new logical(false));
}
/**
* Update break rows information for the given sort criterion.
*
* @param lastValues
* Array which contains the last sort criteria values for all criteria.
* @param lastIndex
* Index of the required criterion in the <code>last</code> array.
* @param currentValue
* Current value of the given sort criterion.
* @param breakRows
* Set of break rows for the given criterion.
* @param currentRow
* Position of the current row into the whole result set.
*/
private void calculateBreakGroups(BaseDataType[] lastValues,
int lastIndex,
BaseDataType currentValue,
Set<Integer> breakRows,
int currentRow)
{
BaseDataType lastBDT = lastValues[lastIndex];
if (lastBDT == null || !currentValue.equals(lastBDT))
{
breakRows.add(currentRow);
// A new "outer" break group necessarily defines a
// new start for all of its "inner" break groups.
if (lastIndex + 1 < lastValues.length)
{
lastValues[lastIndex + 1] = null;
}
lastValues[lastIndex] = currentValue;
}
}
/**
* Push a temporary record context onto a set of buffers. See
* {@link RecordBuffer#pushTempContext()}.
*
* @param buffers
* Buffers on which a temporary record context should be set.
*/
private void pushTempContexts(RecordBuffer[] buffers)
{
for (RecordBuffer buffer : buffers)
{
buffer.pushTempContext();
}
}
/**
* Pop the current, temporary record context from the given buffers.
* <p>
* As a convenience, the currently active temporary records for this
* buffers is set to <code>null</code> first.
*
* @param buffers
* Buffers from which the current temporary record contexts
* should be popped.
*
* @throws PersistenceException
* if setting any current, temporary record to <code>null</code>
* triggers a failure to evict the temporary record from the
* active Hibernate session.
*
* @see #pushTempContexts(RecordBuffer[])
*/
private void popTempContexts(RecordBuffer[] buffers)
throws PersistenceException
{
for (RecordBuffer buffer : buffers)
{
buffer.popTempContext();
}
}
/**
* A helper object which provides services related to sorting and break
* groups. An instance of this class is created for each sort criterion
* added to the query. Each instance contains the <code>DataSorter</code>
* used to implement a particular sort criterion. If break groups are
* enabled, a set of the row numbers for each record which is the first in
* its respective break group is generated during sorting. This set is
* maintained by this object.
*/
private static class SortHelper
{
/** Directional <code>BaseDataType</code> sorter */
private final DataSorter sorter;
/** Optional set of first row numbers per break group */
private Set<Integer> breakRows = null;
/** Evaluator used to obtain break groups information. */
private LazyEvaluator lazyEvaluator = null;
/**
* Constructor which stores the sort value resolver and generates the
* appropriate comparator for an ascending or descending sort.
*
* @param ascending
* <code>true</code> if sort should be ascending;
* <code>false</code> if it should be descending.
*/
SortHelper(boolean ascending)
{
this.sorter = DataSorter.getInstance(ascending);
}
/**
* Store a set of row numbers, each of which represents the first row
* in a break group.
*
* @param breakRows
* Set of break group first row numbers.
*/
void setBreakRows(Set<Integer> breakRows)
{
this.breakRows = breakRows;
}
/**
* Get the set of row numbers, each of which represents the first row
* in a break group.
*
* @return Set of break group first row numbers.
*/
Set<Integer> getBreakRows()
{
return breakRows;
}
/**
* Indicate whether the given row number represents the first row in a
* break group.
*
* @param row
* Zero-based row number to test.
*
* @return <code>true</code> if <code>row</code> represents the first
* row in a break group; else <code>false</code>.
*/
boolean isFirstOfGroup(int row)
{
if (breakRows == null)
{
return false;
}
if (lazyEvaluator != null)
{
// quick check
if (breakRows.contains(row))
{
return true;
}
lazyEvaluator.evaluate(row, true);
}
return breakRows.contains(row);
}
/**
* Indicate whether the given row number represents the last row in a
* break group.
*
* @param row
* Zero-based row number to test.
*
* @return <code>true</code> if <code>row</code> represents the last
* row in a break group; else <code>false</code>.
*/
boolean isLastOfGroup(int row)
{
if (breakRows == null)
{
return false;
}
if (row < 0)
{
return false;
}
if (lazyEvaluator != null)
{
// quick check
if (breakRows.contains(row + 1))
{
return true;
}
lazyEvaluator.evaluate(row, false);
}
return (breakRows.contains(row + 1));
}
/**
* Get the data sorter.
*
* @return Data sorter.
*/
DataSorter getSorter()
{
return sorter;
}
/**
* Indicates that this <code>SortHelper</code> from now should evaluate
* break groups information in the lazy mode using the given evaluator.
*
* @param lazyEvaluator
* Evaluator used to obtain break groups information. It should
* have access to the break rows.
*/
void evaluateLazy(IncrementalResults lazyEvaluator)
{
this.lazyEvaluator = lazyEvaluator;
}
}
/**
* Interface for the classes which are used for lazy evaluation of the break rows.
*/
private interface LazyEvaluator
{
/**
* Evaluate break row(s) which help to determine whether the given row
* if the first/last in a break group.
*
* @param row
* Row to be checked as the beginning/end of a break group.
* @param firstOf
* <code>true</code> indicates that the row should be checked as
* the beginning of a break group. <code>false</code> indicates
* that the row should be checked as the end of a break group
*/
public void evaluate(int row, boolean firstOf);
}
/**
* This class should be used instead of {@link SortedResults} when the sort
* criteria added to the Presorter match the leading components of the
* presort query sort phrase (i.e. results are already sorted in the proper
* order). This class retrieves results iteratively on demand which lead
* to faster initial response / less memory consumption compared to
* {@link SortedResults} which caches and sorts all results up front.
* <p>
* If session expires, all remaining results (record IDs) are cached. Break
* group information in this case will be retrieved iteratively on request.
*/
private class IncrementalResults
extends SimpleResults
implements SessionListener,
LazyEvaluator
{
/** Error message if a non-scrolling query is scrolled backward. */
private final static String BACKWARD_SCROLL_ERROR =
"Backward scroll is not permitted for non-scrolling queries.";
/** Original sorted results. */
private Results backingResults = null;
/**
* Offset that is used to determine the row number into the whole result
* set (global row number = offset + row number into cached result set).
*/
private int backingResultsOffset = 0;
/** Array which contains the sort criteria values for the last loaded row. */
private BaseDataType[] currentBDT = null;
/** Array which contains the sort criteria values for the row before the last loaded row. */
private BaseDataType[] prevBDT = null;
/**
* Array which contains the sort criteria values for the row after the
* last loaded row (i.e. pre-loaded row).
*/
private BaseDataType[] nextBDT = null;
/** Determines whether this <code>Results</code> are scrolling. */
private final boolean scrolling;
/** Number of the row which corresponds the <code>currentBDT</code>. */
private int lastEvaluatedRow = -1;
/** Cache for non-scrolling queries. IDs which correspond with last loaded row. */
private Long[] currentRow = null;
/**
* Cache for non-scrolling queries. IDs which correspond with row before last loaded row.
*/
private Long[] prevRow = null;
/**
* Cache for non-scrolling queries. IDs which correspond with row after last loaded row
* (i.e. pre-loaded row).
*/
private Long[] nextRow = null;
/** Number of loaded rows (for non-scrolling queries). */
private int rowsSize = 0;
/** Indicates whether backing results have been cached because of session closing. */
private boolean resultsCached = false;
/**
* Create a result set that will interrogate <code>sorted</code> results
* iteratively on request.
*
* @param sorted
* Original sorted results.
*/
private IncrementalResults(Results sorted)
{
super(new ArrayList<>());
backingResults = sorted;
if (!(query instanceof PreselectQuery))
{
throw new IllegalStateException("Incremental results was " +
"designed to be used by preselect queries or its descendants!");
}
PreselectQuery preselectQuery = (PreselectQuery) query;
scrolling = preselectQuery.isScrolling() || preselectQuery.isBrowsed();
currentBDT = new BaseDataType[sortHelpers.size()];
for (SortHelper helper : sortHelpers.values())
{
helper.setBreakRows(new HashSet<>());
}
SessionListener.Scope scope =
scrolling || !preselectQuery.isStandalone()
? SessionListener.Scope.ENCLOSING_EXTERNAL
: SessionListener.Scope.CURRENT;
preselectQuery.getPersistence().registerSessionListener(this, scope, preselectQuery.isSharedContext());
}
/**
* Returns the row at the current position. It can be either only an array of PK or
* an array of DMOs.
*
* @param forceOnlyId
* Indicates whether it's only the primary keys that must be returned,
* or original data found is returned (either PK or DMO).
*
* @return Object array or {@code null}.
*/
public Object[] get(boolean forceOnlyId)
{
if (scrolling)
{
return super.get(forceOnlyId);
}
else
{
Object[] datum = (Object[]) currentRow;
// currentRow has only ids, so simply ignore forceOnlyId
return datum;
}
}
/**
* Move cursor to the first results row.
*
* @return <code>true</code> if there are any results.
*/
public boolean first()
{
if (!scrolling && 0 < getRowNumber())
{
throw new UnsupportedOperationException(BACKWARD_SCROLL_ERROR);
}
loadRow(0);
return super.first();
}
/**
* Move cursor to the last results row.
*
* @return <code>true</code> if there are any results.
*/
public boolean last()
{
loadRow(-1);
return super.last();
}
/**
* Move cursor to the next results row.
*
* @return <code>true</code> if there is a result under the cursor
* under the move.
*/
public boolean next()
{
loadRow(getRowNumber() + 1);
return super.next();
}
/**
* Set the row number currently under the cursor.
*
* @param row
* Zero-based index of the row to be set as the current row.
*
* @return <code>true</code> if there is a row at the specified row
* number; else <code>false</code>.
*/
public boolean setRowNumber(int row)
{
if (!scrolling && row < getRowNumber())
{
throw new UnsupportedOperationException(BACKWARD_SCROLL_ERROR);
}
if (row >= 0)
{
loadRow(row);
}
return super.setRowNumber(row);
}
/**
* Scroll the cursor ahead by the specified number of rows. If the
* number is negative, the cursor is moved backward. This may put the
* cursor off the end (either end) of the query's results.
*
* @param rows
* Number of rows to jump ahead or back.
*
* @return <code>true</code> if there is a row at the new location;
* else <code>false</code>.
*/
public boolean scroll(int rows)
{
if (!scrolling && rows < 0)
{
throw new UnsupportedOperationException(BACKWARD_SCROLL_ERROR);
}
if (rows > 0)
{
loadRow(getRowNumber() + rows);
}
return super.scroll(rows);
}
/**
* Returns the number of rows which have been loaded by this
* <code>Results</code>.
*
* @return See above.
*/
@Override
public int getNumberOfLoadedRows()
{
if (!scrolling)
{
return rowsSize;
}
else
{
return super.getNumberOfLoadedRows();
}
}
/**
* Clears the backing results.
*/
public void sessionClosing()
{
cleanupBackingResults();
}
/**
* Clear the internal list of result rows and the backing results.
*/
public void cleanup()
{
super.cleanup();
cleanupBackingResults();
}
/**
* Indicate whether the full result set is cached in this object.
*
* @return {@code true} if all results have been collected and cached, else {@code false}.
*/
@Override
public boolean isResultSetCached()
{
return resultsCached;
}
/**
* Invoked when a Hibernate session event occurs. This implementation
* is interested in receiving events related to the current transaction.
* In the case of a transaction about to commit, this method caches all
* remaining results (record IDs) into a <code>SimpleResults</code>
* instance. On rollback, it just cleans up.
*
* @param event
* Session event type.
*
* @return <code>true</code> in this implemetation (i.e. this listener
* should be removed from the current set of registered
* listeners).
*
* @throws PersistenceException
* if any error occurs during the implementor's processing.
*/
public boolean sessionEvent(SessionListener.Event event)
throws PersistenceException
{
if (backingResults == null)
{
return true;
}
switch (event)
{
case TRANSACTION_COMMITTING:
if (resultsCached && backingResults.isResultSetCached())
{
break;
}
// read all uniterated results from old backingResults into a
// SimpleResults, and set this SimpleResults as new backingResults
ArrayList<Object[]> rows = new ArrayList<>(64);
backingResultsOffset = getNumberOfLoadedRows();
int cols = query.getRecordBuffers().length;
while (backingResults.next())
{
Long[] ids = new Long[cols];
for (int i = 0; i < cols; i++)
{
Record dmo = (Record) backingResults.get(i);
ids[i] = dmo.primaryKey();
}
rows.add(ids);
}
cleanupBackingResults();
backingResults = new SimpleResults(rows);
resultsCached = true;
// notify sort helper that break group information should be
// obtained lazily on request
for (Presorter.SortHelper helper : sortHelpers.values())
{
helper.evaluateLazy(this);
}
break;
case SESSION_CLOSING_NORMALLY:
if (!resultsCached)
{
cleanupBackingResults();
}
break;
case SESSION_CLOSING_WITH_ERROR:
cleanupBackingResults();
break;
default:
return false;
}
return true;
}
/**
* No-op in this implementation.
*/
public void deregisteredSessionListener()
{
}
/**
* Evalualte break row(s) which help to determine whether the given row
* if the first/last in a break group.
*
* @param row
* Row to be checked as the beginning/end of a break group.
* @param firstOf
* <code>true</code> indicates that the row should be checked as
* the beginning of a break group. <code>false</code> indicates
* that the row should be checked as the end of a break group
*/
public void evaluate(int row, boolean firstOf)
{
if (row >= 0 && // 1. we have a valid row
!(isLast(row) && !firstOf) && // 2. it's not an edge case
!(isFirst(row) && firstOf) &&
!(row == lastEvaluatedRow && // 3. we haven't already calculated break group
(firstOf && prevBDT != null ||
!firstOf && nextBDT != null))) // information for this row
{
// we need to store only two history rows to maintain break
// groups information for non-scrolling queries
if (!scrolling)
{
for (SortHelper helper : sortHelpers.values())
{
Iterator<Integer> iter = helper.breakRows.iterator();
while (iter.hasNext())
{
Integer val = iter.next();
if (val < row - 2)
{
iter.remove();
}
}
}
}
try
{
// Our aim is to load the given row and the previous row if we
// want to obtain firstOf(row) information, or load the given
// row and the next row if we want to obtain lastOf(row)
// information. Then we pass data (sort criteria values) into
// calculateBreakGroups(), and it adds a proper information
// to breakRows to make firstOf()/lastOf() return a proper
// result
Long[] ids;
int helpersSize = sortHelpers.size();
if (firstOf)
{
if (row == lastEvaluatedRow - 1 && prevBDT != null)
{
// we have already loaded the given row as the "previous
// row", so make the previous row the current one
currentBDT = prevBDT;
nextBDT = null;
}
else if (row != lastEvaluatedRow)
{
ids = getRow(row);
loadBDTIntoArray(ids, currentBDT);
nextBDT = null;
}
ids = getRow(row - 1);
prevBDT = new BaseDataType[helpersSize];
loadBDTIntoArray(ids, prevBDT);
}
else
{
if (row == lastEvaluatedRow + 1 && nextBDT != null)
{
// we have already loaded the given row as the "next
// row", so make the next row the current one
currentBDT = nextBDT;
prevBDT = null;
}
else if (row != lastEvaluatedRow)
{
ids = getRow(row);
loadBDTIntoArray(ids, currentBDT);
prevBDT = null;
}
ids = getRow(row + 1);
nextBDT = new BaseDataType[helpersSize];
loadBDTIntoArray(ids, nextBDT);
}
int sortCount = sortHelpers.size();
BaseDataType[] prev = new BaseDataType[currentBDT.length];
System.arraycopy(firstOf ? prevBDT : currentBDT,
0,
prev,
0,
currentBDT.length);
BaseDataType[] next = firstOf ? currentBDT : nextBDT;
Iterator<SortHelper> iter = sortHelpers.values().iterator();
for (int i = 0; i < sortCount; i++)
{
SortHelper helper = iter.next();
calculateBreakGroups(prev,
i,
next[i],
helper.getBreakRows(),
firstOf ? row : row + 1);
}
lastEvaluatedRow = row;
}
catch (PersistenceException e)
{
ErrorManager.recordOrThrowError(e);
}
}
}
/**
* Clear the backing results.
*/
private void cleanupBackingResults()
{
if (backingResults != null)
{
backingResults.cleanup();
backingResults = null;
}
}
/**
* Load the specified row into the cache.
*
* @param rowNumber
* Position of the requested row in the results set.
*/
private void loadRow(int rowNumber)
{
if (backingResults == null)
{
return;
}
boolean iterateToLast = rowNumber == -1;
// pre-load an extra row to get the break group information
rowNumber++;
int actualRow = backingResultsOffset + backingResults.getRowNumber();
if (iterateToLast || actualRow < rowNumber)
{
int rowsToIterate = rowNumber - actualRow;
RecordBuffer[] buffers = query.getRecordBuffers();
int cols = buffers.length;
// push temp context for all buffers
if (!resultsCached)
{
pushTempContexts(buffers);
}
try
{
try
{
for (int i = 0; iterateToLast || i < rowsToIterate; i++)
{
boolean res = backingResults.next();
if (!res)
{
// add dummy row to push the last row to the zero position
if (!scrolling)
{
addRow(null);
}
cleanupBackingResults();
return;
}
else
{
Long[] ids = new Long[cols];
for (int j = 0; j < cols; j++)
{
if (!resultsCached)
{
Record dmo = (Record) backingResults.get(j);
ids[j] = dmo.primaryKey();
buffers[j].setTempRecord(dmo);
}
else
{
ids[j] = (Long) backingResults.get(j);
}
}
addRow(ids);
if (!resultsCached)
{
int sortCount = sortHelpers.size();
// Collect sort criteria values.
Iterator<Map.Entry<Resolvable, SortHelper>> iter =
sortHelpers.entrySet().iterator();
for (int j = 0; j < sortCount; j++)
{
Map.Entry<Resolvable, SortHelper> entry = iter.next();
Resolvable resolver = entry.getKey();
SortHelper helper = entry.getValue();
Set<Integer> breakRows = helper.getBreakRows();
int currentRow = backingResults.getRowNumber();
// we need to store only two rows to maintain
// break groups information for non-scrolling
// queries
if (!scrolling)
{
breakRows.remove(currentRow - 2);
}
calculateBreakGroups(currentBDT,
j,
resolver.resolve(),
breakRows,
currentRow);
}
}
}
}
}
finally
{
// Clear buffers' temporary records.
if (!resultsCached)
{
popTempContexts(buffers);
}
}
}
catch(PersistenceException e)
{
ErrorManager.recordOrThrowError(e);
}
}
}
/**
* Store the given record identifiers to the rows cache.
*
* @param ids
* Identifiers to be stored.
*/
private void addRow(Long[] ids)
{
if (scrolling)
{
List<Object[]> rowData = getRows();
rowData.add(ids);
}
else
{
if (currentRow == null)
{
currentRow = ids;
}
else if (nextRow == null)
{
nextRow = ids;
}
else
{
prevRow = currentRow;
currentRow = nextRow;
nextRow = ids;
}
if (ids != null)
{
rowsSize++;
}
}
}
/**
* Get the specified row (an array of record identifiers).
*
* @param row
* Number of row to return.
* @return See above.
*/
private Long[] getRow(int row)
{
Object datum;
if (scrolling)
{
datum = getRows().get(row);
}
else
{
if (row == getRowNumber())
{
datum = currentRow;
}
else if (row == getRowNumber() + 1)
{
datum = nextRow;
}
else if (row == getRowNumber() - 1)
{
datum = prevRow;
}
else
{
throw new IllegalStateException("For non-scrolling query it " +
"is possible to reach only current, next or previous row.");
}
}
if (datum instanceof Long[])
{
return (Long[]) datum;
}
return (new Long[] { (Long) datum });
}
/**
* Tests whether the given row is the first row in the results set.
*
* @param row
* Row to test.
*
* @return <code>true</code> if the given row is the first row.
*/
private boolean isFirst(int row)
{
return row == 0;
}
/**
* Tests whether the given row is the last row in the results set.
*
* @param row
* Row to test.
*
* @return <code>true</code> if the given row is the last row.
*/
private boolean isLast(int row)
{
return (row >= 0 && row == getNumberOfLoadedRows() - 1);
}
/**
* Load sort criteria values using identifiers of corresponding records.
*
* @param ids
* Array of record identifiers.
* @param targetArray
* Array for storing sort criteria values.
*
* @throws PersistenceException
* if there was an error creating a Hibernate session or getting
* a record or record eviction problems.
*/
private void loadBDTIntoArray(Long[] ids, BaseDataType[] targetArray)
throws PersistenceException
{
RecordBuffer[] buffers = query.getRecordBuffers();
int cols = buffers.length;
pushTempContexts(buffers);
try
{
Persistence persistence = ((PreselectQuery) query).getPersistence();
for (int i = 0; i < cols; i++)
{
String entity = buffers[i].getEntityName();
RecordIdentifier<String> ident = new RecordIdentifier<>(entity, ids[i]);
Record dmo = persistence.quickLoad(ident, !buffers[i].getDmoInfo().multiTenant);
buffers[i].setTempRecord(dmo);
}
int sortCount = sortHelpers.size();
// Collect sort criteria values.
Iterator<Resolvable> iter = sortHelpers.keySet().iterator();
for (int j = 0; j < sortCount; j++)
{
Resolvable resolver = iter.next();
targetArray[j] = resolver.resolve();
}
}
finally
{
popTempContexts(buffers);
}
}
}
/**
* A specialized implementation of <code>Results</code> which
* <i>presorts</i> all query results according to the sort criteria
* specified for this query. The sorting takes place during construction
* of this object.
*/
private class SortedResults
extends SimpleResults
{
/** Comparator used to presort rows */
private final RowComparator comparator = new RowComparator();
/**
* Constructor which presorts all results according to this query's
* sort criteria and retains a list of the primary key ID arrays
* representing the records which comprise each result row. If break
* groups are enabled for this query, the set of leading rows for each
* break group is generated as well.
* <p>
* The sort is performed by accessing each result row returned by the
* initial query execution. These rows are in no particular order, but
* are visited from first to last. Each row consists of one or more
* records. Each such record is loaded temporarily into its associated
* record buffer. The resolvable data source for each sort criterion
* is then asked to resolve its sort value, and these values are stored
* into a sort key array in a temporary,
* <code>PresortQuery.SortedResults.Row</code> object within a list.
* Once sort keys have been generated for all result rows, the list is
* then sorted, primary key arrays for each row are preserved, and the
* sort keys and original result set are discarded.
*
* @param results
* Scrollable result set generated by the query. Unlike the
* scrollable result set used by <code>PreselectQuery</code>,
* this result set could contain full data records or primary key IDs.
* @param loadDMOs
* When the row contains only the PK, indicate if the record
* should be loaded into its associated record buffer.
*
* @throws PersistenceException
* if there is an error evicting a temporary record from the
* persistence session after it is used.
*/
private SortedResults(Results results, boolean loadDMOs)
throws PersistenceException
{
super(new ArrayList<>());
List<Object[]> rowData = getRows();
List<Row> rows = new ArrayList<>();
RecordBuffer[] buffers = query.getRecordBuffers();
int cols = buffers.length;
int sortCount = sortHelpers.size();
// Collect buffers.
pushTempContexts(buffers);
try
{
for (int x = 0; results.next(); x++)
{
// collect primary keys and prepare buffers with temporary records
Long[] ids = new Long[cols];
for (int i = 0; i < cols; i++)
{
Object res = results.get(i);
Record dmo = null;
Long pk = null;
if (res instanceof Record)
{
dmo = (Record) res;
pk = dmo.primaryKey();
}
else if (res instanceof Long)
{
pk = (Long) res;
if (loadDMOs)
{
String entity = buffers[i].getEntityName();
RecordIdentifier<String> ident = new RecordIdentifier<>(entity, pk);
dmo = buffers[i].getPersistence().quickLoad(ident, !buffers[i].getDmoInfo().multiTenant);
}
}
buffers[i].setTempRecord(dmo);
// a null DMO is legal (in the case of an outer join)
ids[i] = pk;
}
// Collect sort key results.
BaseDataType[] sortKey = new BaseDataType[sortCount];
Iterator<Resolvable> iter2 = sortHelpers.keySet().iterator();
for (int i = 0; iter2.hasNext(); i++)
{
Resolvable resolver = iter2.next();
sortKey[i] = resolver.resolve();
}
Row row = new Row(sortKey, ids);
// Add the row to the (still unsorted) list of rows.
rows.add(row);
// For large result sets, we could be in this loop for a long
// while; test here to honor thread interruptions. This
// requires multiple JNI transitions, so we don't do it on
// every pass.
if (x % 100 == 0)
{
TransactionManager.honorStopCondition();
}
}
}
finally
{
results.cleanup();
// Clear buffers' temporary records.
popTempContexts(buffers);
}
if (!rows.isEmpty())
{
// sort the rows using the resolved sort keys and registered comparators
rows.sort(comparator);
// Drop any unneeded records in the case of FIRST/LAST iteration.
// filterResults(rows);
// Replace interim Row objects with final primary key IDs.
// rowData.ensureCapacity(rows.size());
for (Row row : rows)
{
rowData.add(row.ids);
}
// Drop sort criteria and set up break group list.
BaseDataType[] last = new BaseDataType[sortCount];
Set<Integer>[] breakRows = (Set<Integer>[]) new Set[sortCount];
if (breakGroups)
{
for (int i = 0; i < sortCount; i++)
{
breakRows[i] = new HashSet<>();
}
}
int size = rows.size();
for (int i = 0; i < size; i++)
{
Row row = rows.get(i);
if (breakGroups)
{
// Record the first row number of each break group.
for (int j = 0; j < sortCount; j++)
{
calculateBreakGroups(last, j, row.sortKey[j], breakRows[j], i);
}
}
}
if (breakGroups)
{
Iterator<SortHelper> iter2 = sortHelpers.values().iterator();
for (int i = 0; iter2.hasNext(); i++)
{
SortHelper helper = iter2.next();
helper.setBreakRows(breakRows[i]);
}
}
}
// rowData.trimToSize();
}
/**
* In cases where iteration type is not EACH (i.e., FIRST, LAST), drop
* the unneeded records.
*
* @param rows
* Unfiltered (but sorted) results.
*/
/*
private void filterResults(List<Row> rows)
{
boolean filter = false;
int tableCount = getTableCount();
int[] modes = new int[tableCount];
Iterator<Component> iter = components();
for (int i = 0; iter.hasNext(); i++)
{
Component next = iter.next();
modes[i] = next.getIteration();
if (modes[i] != NEXT)
{
filter = true;
}
}
ArrayList<Serializable[]> rowData =
(ArrayList<Serializable[]>) getRows();
if (!filter)
{
for (Row row : rows)
{
rowData.add(row.ids);
}
return;
}
Serializable[] prevIDs = new Serializable[tableCount];
Serializable[] nextIDs = new Serializable[tableCount];
Iterator<Row> iter2 = rows.iterator();
boolean keepNext = true;
boolean dropPrev = false;
for (int i = 0; iter2.hasNext(); i++)
{
Row row = iter2.next();
nextIDs = row.ids;
keepNext = true;
tableLoop:
for (int j = 0; j < tableCount; j++)
{
switch (modes[j])
{
case NEXT:
break;
case FIRST:
if (j == 0)
{
// If we are the outermost table, drop all rows after
// the first.
if (i > 0)
{
keepNext = false;
break tableLoop;
}
}
else
{
// If we are on the right side of a join, drop any row
// where the ID of the left side is the same as that
// of the previous row.
int x = j - 1;
if (i > 0 && nextIDs[x] == prevIDs[x])
{
keepNext = false;
break tableLoop;
}
}
break;
case LAST:
if (prevIDs != null)
{
if (j == 0)
{
dropPrev = true;
}
else
{
int x = j - 1;
if (prevIDs[x] == nextIDs[x])
{
dropPrev = true;
}
}
}
break;
default:
break;
}
}
if (dropPrev)
{
int x = rowData.size() - 1;
if (x >= 0)
{
rowData.remove(x);
prevIDs = (x > 0 ? rowData.get(x - 1) : null);
}
}
if (keepNext)
{
rowData.add(nextIDs);
prevIDs = nextIDs;
}
}
}
*/
/**
* A transient object which is used only during the presorting phase
* and which is discarded once the results have been sorted (at which
* time it is replaced with only the array of primary key IDs).
*/
private class Row
{
/** Array of <code>Resolvable</code> results used for sorting */
private final BaseDataType[] sortKey;
/** Array of primary keys of result row records */
private final Long[] ids;
/**
* Convenience constructor.
*
* @param sortKey
* Array of <code>Resolvable</code> results used for
* sorting.
* @param ids
* Array of primary keys of result row records.
*/
Row(BaseDataType[] sortKey, Long[] ids)
{
this.sortKey = sortKey;
this.ids = ids;
}
}
/**
* Specialized comparator which uses the sort criteria previously added
* to the query, along with the corresponding values resolved using
* those criteria against each result row.
*/
private class RowComparator
implements Comparator<Row>
{
/**
* Compare two objects, where each is expected to be an instance of
* <code>PresortQuery.SortedResults.Row</code>.
* <p>
* For each comparison, the sort key array is extracted from each
* row. Each pair of elements in the arrays is compared using the
* corresponding sort criterion comparator, until an unequivalent
* result is obtained, or until no further elements remain (and the
* rows consequently are determined to sort at the same precedence).
*
* @param r1
* First row to be compared.
* @param r2
* Second row to be compared.
*
* @return Negative number if <code>r1</code> is less than <code>r2</code>;
* positive number if <code>r1</code> is greater than <code>r2</code>;
* <code>0</code> if they compare as equivalent.
*/
public int compare(Row r1, Row r2)
{
BaseDataType[] key1 = r1.sortKey;
BaseDataType[] key2 = r2.sortKey;
Iterator<SortHelper> iter = sortHelpers.values().iterator();
for (int i = 0; iter.hasNext(); i++)
{
SortHelper helper = iter.next();
int result = helper.getSorter().compare(key1[i], key2[i]);
if (result != 0)
{
return result;
}
}
return 0;
}
}
}
}