PresortCompoundQuery.java
/*
** Module : PresortCompoundQuery.java
** Abstract : Implementation of a presorting, multi-table query
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 ECF 20080112 @36991 Created initial version. Implementation of a
** presorting, multi-table query which does not
** perform its table join(s) on the database
** server.
** 002 ECF 20090313 @41602 Forced full-records mode. Added overridden
** addComponent() method which calls
** setFullRecords().
** 003 SVL 20090723 @43343 Changes due to Sorter API change.
** 004 SIY 20100303 @44700 Added support for proper accumulator handling.
** 005 SVL 20111111 Make the class implement PresortDelegate.
** 006 ECF 20131028 Import change.
** 007 ECF 20150327 Javadoc fix.
** 008 OM 20150727 Synchronized the cursors for first/next/prev/last methods.
** 009 ECF 20150806 Re-ordered some methods to comply with coding standards.
** 010 SVL 20160526 Removed sorted result set (second). All sorted data is now stored in
** the cursor.
** 011 GES 20160919 All initialization is now done with a default constructor followed
** by a call to initialize(). This enables query instance members in
** converted code to be local variables that are effectively final.
** This is needed to support lambdas without moving queries to be
** instance members. Queries as instance members breaks recursion
** use cases.
** 012 OM 20180620 Added support for dynamically configured sort criteria.
** 013 ECF 20181106 Added runtime support for {FIRST|LAST}-OF methods.
** 014 ECF 20200906 New ORM implementation.
** 015 IAS 20230717 Added 'initialize' with two arguments.
** 016 IAS 20230921 Added initializa2f() method.
** 017 ES 20240404 Overridden isPesort method.
** 018 CA 20240422 Allow the query to be reset to its initialization state, so the same instance can
** be used to execute child buffer FILL operations.
** 019 AL2 20240926 Adapted CursorResults to new signature of get.
** 020 LS 20241101 Adapted Presorter to new signature of createSortedResults.
*/
/*
** 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 com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.util.*;
/**
* PresortCompoundQuery is used when it is necessary to presort the results
* of a multi-table join, where the join cannot be performed at the database
* server. This is necessary when tables are being joined across databases,
* for example, though there may be other circumstances which could drive
* this requirement.
* <p>
* Upon the first request for a record, this query preselects all of its
* results in the order prescribed by each of its individual query components.
* It then sorts these results according to the sort criteria provided by one
* of the <code>addSortCriterion()</code> method variants. These results are
* cached. The appropriate record is returned for the original request, and
* thereafter, the cache is accessed for all subsequent record retrieval
* requests. The cache contains only primary key ID values; the actual
* records associated with these are retrieved on demand.
*/
public final class PresortCompoundQuery
extends CompoundQuery
implements Presortable,
PresortDelegate
{
/** Helper which sorts preselected, unsorted results */
private Presorter sorter;
/** The cached results stored in the cursor, wrapped by {@link Results} interface. */
private CursorResults cursorResults = null;
/**
* Default constructor. Initialization is deferred until <code>initialize()</code> is
* called.
*/
public PresortCompoundQuery()
{
}
/**
* Clear the current results from the query, so the same query instance can be executed again, without
* having to {@link #open() re-open} it.
*/
@Override
public void clearResults()
{
super.clearResults();
this.cursorResults = null;
this.sorter = null;
}
/**
* Initialization logic.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param resolveOnce
* {@code true} to resolve arguments once, up front, or on every loop cycle.
* @param preselect
* {@code true} to force preselection of all results; {@code false} otherwise.
* @return This query instance.
*/
public PresortCompoundQuery initialize(boolean resolveOnce, boolean preselect)
{
// Resolve args once up front and preselect results.
super.initialize(resolveOnce, preselect);
sorter = new Presorter(this);
setScrolling(); // we need cursor in any case, so set the query scrolling even if it
// backs a cycle
return this;
}
/**
* Initialization logic.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @return This query instance.
*/
public PresortCompoundQuery initialize()
{
return initialize(true, true);
}
/**
* Initialization logic.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @return This query instance.
*/
public PresortCompoundQuery initialize2f()
{
return initialize(false, false);
}
/**
* Add a joinable query to the compound query, and specify the nature of
* the iteration which will be performed (first, last, next, etc.) on that
* component, when the compound query is iterated. If this is not the
* first component to be added to the query, the type of join performed
* with the immediately previous component is determined by the value of
* <code>outer</code>.
*
* @param query
* Query which will perform the fundamental work for this
* component when the compound query is iterated.
* @param iteration
* A {@link QueryConstants constant} indicating the type of work
* to be performed upon an iteration of the compound query. This
* parameter determines whether the underlying query will be asked
* to perform a first, last, next, previous, or unique record
* retrieval.
* @param outer
* <code>true</code> if this component should be joined to the
* previously added component via a left outer join;
* <code>false</code> if an inner join is required. Note that
* adding an inner join component will cause any previously added
* components added as outer joins to be reset to inner joins.
*
* @throws IllegalStateException
* if this method is invoked during an iteration cycle.
*
* @see #iterate
*/
public void addComponent(Joinable query, int iteration, boolean outer)
{
query.setFullRecords();
super.addComponent(query, iteration, outer);
}
/**
* Access the record buffers manipulated by this query, in order from
* left-most (outermost) table of the query's join (if any) to right-most
* (innermost) table.
*
* @return Array of one or more record buffers.
*/
public RecordBuffer[] getRecordBuffers()
{
RecordBuffer[] buffers = new RecordBuffer[getTableCount()];
Iterator<RecordBuffer> iter = recordBuffers();
for (int i = 0; iter.hasNext(); i++)
{
buffers[i] = iter.next();
}
return buffers;
}
/**
* 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.
*/
public void addSortCriterion(Resolvable sort)
{
sorter.addSortCriterion(sort);
}
/**
* 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.
* @param descending
* if <code>true</code>, the sort is descending, from highest
* resolved value to lowest; otherwise, the sort is ascending.
*/
public void addSortCriterion(Resolvable sort, boolean descending)
{
sorter.addSortCriterion(sort, descending);
}
/**
* Add a single dynamic 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.
* @param ascending
* if {@code true}, the sort is ascending, from lowest resolved value to highest;
* otherwise, the sort is descending.
*/
@Override
public void addDynamicSortCriterion(Resolvable sort, boolean ascending)
{
sorter.addDynamicSortCriterion(sort, ascending);
}
/**
* Clears all sort criteria added dynamically.
*/
@Override
public void clearDynamicSortCriteria()
{
sorter.clearDynamicSortCriteria();
}
/**
* 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.
*/
public void enableBreakGroups()
{
sorter.enableBreakGroups();
}
/**
* 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>.
*/
public logical isFirst()
{
return sorter.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.
*/
public logical isFirstOfGroup(Resolvable key)
{
return sorter.isFirstOfGroup(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.
*
* @return {@code true} if the current row is first in the result set or within the specified
* break group; else {@code false}.
*/
public logical isFirstOfGroup(integer level)
{
return sorter.isFirstOf(level);
}
/**
* 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.
*
* @return {@code true} if the current row is first in the result set or within the specified
* break group; else {@code false}.
*/
public logical isFirstOfGroup(int level)
{
return isFirstOfGroup(new integer(level));
}
/**
* 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>.
*/
public logical isLast()
{
return sorter.isLast();
}
/**
* Indicate whether the query is of presort type.
*
* @return <code>true</code> if the query is of presort type..
*/
@Override
public boolean isPresort()
{
return true;
}
/**
* 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.
*/
public logical isLastOfGroup(Resolvable key)
{
return sorter.isLastOfGroup(key);
}
/**
* 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.
* Must not be unknown value.
*
* @return {@code true} if the current row is last in the result set or within the specified
* break group; else {@code false}.
*/
public logical isLastOfGroup(integer level)
{
return sorter.isLastOf(level);
}
/**
* 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}.
*/
public logical isLastOfGroup(int level)
{
return isLastOfGroup(new integer(level));
}
/**
* Access the current, sorted results for this query.
*
* @return <code>Results</code> object used to retrieve sorted results.
*/
public Results getSortedResults()
{
return cursorAsResults();
}
/**
* Retrieve the next virtual, composite row of results for the compound
* query, overriding the lock type to apply to each underlying query.
* <p>
* This is a combination of records retrieved by each component query. If
* the compound query involves outer joins, some of the underlying buffers
* may be empty when this method returns.
*
* @param iterating
* <code>true</code> if this method is being invoked within the
* context of an iterating loop, which can only be exited by
* raising an end condition; <code>false</code> if end condition
* should not be raised, even if no further advancement of the
* composite result is possible.
* @param lockType
* Lock type which should override that of any underlying query.
*
* @throws QueryOffEndException
* if no more results were available in iterating mode.
* @throws ErrorConditionException
* if an underlying query triggers an error.
*/
public boolean next(boolean iterating, LockType lockType)
{
boolean hasNext = super.next(iterating, lockType);
accumulate();
return hasNext;
}
/**
* 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.
*/
protected boolean isLastOfBreakGroup(Resolvable key)
{
return sorter.isLastOfBreakGroup(key);
}
/**
* 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.
*/
protected boolean isNewBreakGroup(Resolvable key)
{
return sorter.isNewBreakGroup(key);
}
/**
* Determines if the query is presorted, i.e. if the query is sorted up front in some special
* way.
*
* @return <code>true</code> for this implementation.
*/
protected boolean isPresorted()
{
return true;
}
/**
* Get the cached results stored in the cursor, wrapped by {@link Results} interface.
*
* @return the cached results stored in the cursor, wrapped by {@link Results} interface.
*/
protected CursorResults cursorAsResults()
{
if (cursorResults == null)
{
cursorResults = new CursorResults(cursor);
}
return cursorResults;
}
/**
* Presort the results stored in the cursor.
*/
protected void presort()
{
try
{
if (hasAccumulators())
{
postponeAccumulation();
}
// reset cursor's position, the sorter processes rows from this position
cursor.beforeFirst();
// sort them
SimpleResults sorted = (SimpleResults) sorter.createSortedResults(cursorAsResults(), true);
// discard unsorted cursor results and set sorted instead
cursor.setResults(sorted.getRows());
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
finally
{
if (hasAccumulators())
{
restoreAccumulation();
}
}
}
/**
* An implementation of the <code>Results</code> interface which is backed
* by a dynamic {@link Cursor} object.
* <p>
* Only those methods which are required by the {@link Presorter} to sort
* the results managed by the cursor are implemented.
*/
private static class CursorResults
implements Results
{
/** Backing cursor */
private final Cursor cursor;
/** Array of records which represents the current, virtual row */
private Object[] row = null;
/**
* Constructor which accepts the backing cursor object.
*
* @param cursor
* Cursor which backs this <code>Results</code> object.
*/
CursorResults(Cursor cursor)
{
this.cursor = cursor;
}
/**
* Move cursor to the first results row.
*
* @return <code>true</code> if there are any results.
*/
public boolean first()
{
row = cursor.getFirst();
return (row != null);
}
/**
* Move cursor to the last results row.
*
* @return <code>true</code> if there are any results.
*/
public boolean last()
{
row = cursor.getLast();
return (row != null);
}
/**
* 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()
{
row = cursor.getNext();
return (row != null);
}
/**
* Move cursor to the previous results row.
*
* @return <code>true</code> if there is a result under the cursor
* under the move.
*/
public boolean previous()
{
row = cursor.getPrevious();
return (row != null);
}
/**
* Is the cursor on the first row in the results set?
*
* @return <code>true</code> if the cursor is on the first row.
*/
public boolean isFirst()
{
return getRowNumber() == 0;
}
/**
* Is the cursor on the last row in the results set?
*
* @return <code>true</code> if the cursor is on the first row.
*/
public boolean isLast()
{
return cursor.size().intValue() == getRowNumber() + 1;
}
/**
* 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}.
*/
@Override
public Object[] get(boolean forceOnlyId)
{
if (!forceOnlyId)
{
return row;
}
Object[] newRow = new Object[row.length];
for (int i = 0; i < row.length; i++)
{
if (row[i] instanceof Record)
{
newRow[i] = ((Record) row[i]).primaryKey();
}
else
{
newRow[i] = row[i];
}
}
return newRow;
}
/**
* Get the object at the current result row, at the specified column.
*
* @param column
* Zero-based index column of the desired object.
*
* @return Object at <code>column</code> or <code>null</code>.
*/
public Object get(int column)
{
return (row == null ? null : row[column]);
}
/**
* Get the primary key ID at the current result row, at the specified
* column.
*
* @param column
* Zero-based index column of the desired ID.
*
* @return ID of the record or <code>null</code>.
*/
public Long getID(int column)
{
Object datum = get(column);
if (datum instanceof Record)
{
return ((Record) datum).primaryKey();
}
return ((Long) datum);
}
/**
* Get the row number currently under the cursor.
*
* @return Zero-based index of the current row, or <code>-1</code>
* if the cursor is not currently on a result.
*/
public int getRowNumber()
{
integer position = cursor.currentRow();
if (position.isUnknown())
{
return -1;
}
return position.intValue() - 1;
}
/**
* 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>.
*
* @throws UnsupportedOperationException
* always; this method is not needed to sort results.
*/
public boolean setRowNumber(int row)
{
throw new UnsupportedOperationException();
}
/**
* 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>.
*
* @throws UnsupportedOperationException
* always; this method is not needed to sort results.
*/
public boolean scroll(int rows)
{
throw new UnsupportedOperationException();
}
/**
* Reset the cursor to its natural starting position, before the first
* result row.
*/
public void reset()
{
cursor.beforeFirst();
}
/**
* Invoked when the current Hibernate session is about to close. Gives
* the implementation an opportunity to perform appropriate processing
* in response to this event.
* <p>
* This implementation does nothing.
*/
public void sessionClosing()
{
}
/**
* Clean up and release any resources which this object is holding, such
* as open result sets.
* <p>
* This implementation does nothing.
*/
public void cleanup()
{
}
}
}