FilteredResults.java
/*
** Module : FilteredResults.java
** Abstract : Results object which filters records against client-side where expressions
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060730 @28265 Moved from PreselectQuery class. Results object which filters
** records against client-side where expressions.
** 002 ECF 20060803 @28390 Fixed defect in filtering. Previously only one filter had to be
** passed for a row to be accepted. Now a row must pass all filters.
** 003 ECF 20070412 @32978 Use RecordIdentifier instances for record locking. We don't use the
** RecordLockContext in this case, since we are not editing, and don't
** want to pin locks open any longer than necessary.
** 004 ECF 20070711 @34452 Added sessionClosing() method. Delegates the handling of the session
** close event to the underlying, unfiltered Results object.
** 005 ECF 20070815 @34854 Support converted nested CAN-FIND statements. Use RecordBuffer's new
** API to push/pop temp record contexts.
** 006 ECF 20080110 @37002 Removed reference to RecordLockContext, which was unused.
** 007 ECF 20090313 @41597 Added idsOnly to constructor. Removed method storeIDsOnly(). This
** provides more flexibility for queries using this class.
** 008 ECF 20090416 @42977 Minor code cleanup.
** 009 ECF 20131028 Import change.
** 010 SVL 20141020 Update to reflect off-end changes in SimpleResults.
** 011 GES 20160804 Switched from WhereExpression to lambda usage.
** 012 ECF 20190427 Fixed results caching.
** 013 AIL 20200410 Fixed evicting results from persistence.
** 20200415 Replaced evicting fix with deepCopy fix.
** 20200422 Changed isResultSetCached flag to be delivered by unfiltered.
** 014 ECF 20200906 New ORM implementation.
** 015 AL2 20241017 Hydrate records from unfiltered results. This can happen when the results
** were cached due to a full transaction and we preferred to save only the PK
** to avoid getting all the data in memory.
** 016 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
*/
/*
** 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.persist.lock.*;
import com.goldencode.p2j.util.*;
/**
* An implementation of the <code>Results</code> interface which evaluates
* one or more client-side where clause expressions per virtual record, to
* create a filtered list of records.
*/
class FilteredResults
extends SimpleResults
{
/** Results to be filtered */
private final Results unfiltered;
/** Record buffer(s) used during filtering */
private final List<RecordBuffer> buffers;
/** Client-side where expressions which define the filter */
private final Collection<Supplier<logical>> filters;
/** Store primary key IDs only or full records in filtered results? */
private final boolean idsOnly;
/**
* Construct an instance of this object from a first-pass result set
* provided by query execution. Results will be filtered down to only
* that set which meet the criteria of the query's client-side where
* expression. Thus, each record in the first-pass result set is
* retrieved and processed against the where expression; those which
* pass the filter are retained.
*
* @param unfiltered
* Unfiltered result set generated by the initial query.
* @param buffers
* Record buffer(s) used during filtering.
* @param filters
* Client-side where expressions which define the filter.
* @param idsOnly
* If <code>true</code>, only primary key IDs are stored in the
* filtered results; if <code>false</code>, full records are
* stored.
*/
FilteredResults(Results unfiltered,
List<RecordBuffer> buffers,
Collection<Supplier<logical>> filters,
boolean idsOnly)
{
super(new LinkedList<>());
this.unfiltered = unfiltered;
this.buffers = buffers;
this.filters = filters;
this.idsOnly = idsOnly;
}
/**
* Move cursor to the first results row.
*
* @return <code>true</code> if there are any results.
*/
public boolean first()
{
if (super.first())
{
return true;
}
return next();
}
/**
* Move cursor to the last results row. This can be an expensive
* operation, depending upon the implementation of the underlying,
* unfiltered results object. It requires we visit each record from the
* last known, filtered record, until no more records can be found, then
* move back one position.
*
* @return <code>true</code> if there are any results.
*/
public boolean last()
{
super.last();
while (next())
/* empty block */;
return super.last();
}
/**
* Move cursor to the next results row.
* <p>
* If we have not previously found the next (filtered) record, advance
* through the unfiltered results until we find a record which matches the
* filter criteria or until we run out of test records. The underlying,
* unfiltered results will be left at the matching record or off-end,
* respectively when this method completes.
* <p>
* Non-matching records are evicted from the persistence session, unless
* other buffers in the current context are using them.
*
* @return <code>true</code> if there is a result under the cursor
* after the move.
*/
public boolean next()
{
// If we already have advanced beyond the current position previously,
// we will already have cached the next record, so try to retrieve it
// from the cache first.
if (super.next())
{
return true;
}
// We are at back off-end at this point.
// Do it the hard way: advance the unfiltered result set in a loop and
// test each record returned against all registered where expressions.
// First match stops the loop and leaves the results object at the
// matching record and the underlying results object on the last record
// tested.
try
{
int len = buffers.size();
Long[] ids = new Long[len];
LockType[] oldLocks = new LockType[len];
// Cache table names and Persistence objects.
String[] tables = new String[len];
Persistence[] persistences = new Persistence[len];
Iterator<RecordBuffer> iterB = buffers.iterator();
for (int i = 0; iterB.hasNext(); i++)
{
RecordBuffer buffer = iterB.next();
tables[i] = buffer.getTable();
persistences[i] = buffer.getPersistence();
buffer.pushTempContext();
}
outer:
while (unfiltered.next())
{
try
{
iterB = buffers.iterator();
for (int i = 0; iterB.hasNext(); i++)
{
RecordBuffer buffer = iterB.next();
Object next = unfiltered.get(i);
String table = tables[i];
Persistence persistence = persistences[i];
boolean sharedDb = !buffer.getDmoInfo().multiTenant;
if (!(next instanceof Record))
{
// rare case in which the unfiltered results are actual PK; this can happen when
// the unfiltered results were "shrink" to their PK to avoid large memory consumption
next = persistence.getSession(sharedDb)
.get(buffer.getDMOImplementationClass(), (Long) next);
}
Record dmo = (Record) next;
Long id = dmo.primaryKey();
ids[i] = id;
// Share-lock the record so that it is not altered while
// we are testing it, but only if it is currently not
// locked by this context.
RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);
LockType oldLock = persistence.queryLock(ident, sharedDb);
if (oldLock.equals(LockType.NONE))
{
// NOTE: we don't use the RecordLockContext here, since
// we are not making edits (ever), and we don't want to
// hold the share lock any longer than is absolutely
// necessary to ensure a safe check in the where
// expression.
oldLocks[i] = oldLock;
persistence.lock(LockType.SHARE, ident, true, sharedDb);
}
else
{
oldLocks[i] = null;
}
buffer.setTempRecord((Record) dmo.deepCopy());
}
// Test the record against all registered where expressions.
Iterator<Supplier<logical>>iterW = filters.iterator();
while (iterW.hasNext())
{
Supplier<logical> testExpr = iterW.next();
if (!testExpr.get().booleanValue())
{
continue outer;
}
}
// If we got here, we passed all filters; add the row to the
// filtered results.
Object[] row = null;
if (idsOnly)
{
row = ids;
}
else
{
row = new Object[len];
for (int i = 0; i < len; i++)
{
row[i] = unfiltered.get(i);
}
}
getRows().add(row);
// We just added the next record above and the position corresponds this record
// (we were off-end).
return true;
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
finally
{
// Restore previous lock status if necessary.
for (int i = 0; i < len; i++)
{
LockType oldLock = oldLocks[i];
if (oldLock != null)
{
RecordIdentifier<String> ident = new RecordIdentifier<>(tables[i], ids[i]);
persistences[i].lock(oldLock, ident, true, !buffers.get(i).getDmoInfo().multiTenant);
}
oldLocks[i] = null;
}
}
}
}
catch (LockUnavailableException luExc)
{
// OK to ignore. We never request a no-wait lock, so this will
// never be thrown by the lock manager.
}
finally
{
// Remove temp records from buffers.
Iterator<RecordBuffer> iter = buffers.iterator();
PersistenceException error = null;
while (iter.hasNext())
{
try
{
iter.next().popTempContext();
}
catch (PersistenceException exc)
{
if (error == null)
{
error = exc;
}
}
}
if (error != null)
{
ErrorManager.recordOrThrowError(error);
}
}
return false;
}
/**
* Determine whether the cursor is on the last row in the results set.
* This attempts to advance the cursor by one position temporarily to see
* if there is a record following the current record.
*
* @return <code>true</code> if the cursor is on the first row.
*/
public boolean isLast()
{
boolean last = false;
if (getRowNumber() >= 0)
{
if (next())
{
// If we could advance one, we were not at the last record yet.
last = false;
// Reset to where we were.
previous();
}
else
{
// If we could not advance one, we were at the last record...
last = true;
// ...and now we're off-end, so get us back on the last record.
super.last();
}
}
return last;
}
/**
* Set the row number currently under the cursor. This will attempt to
* move to the specified row number, even if it is off the end of the
* results.
* <p>
* Scrolling to a previously visited row (in either direction) is an
* inexpensive operation. Scrolling forward to a new row may be an
* expensive operation, since it may require the retrieval and testing of
* many records.
*
* @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)
{
int currentRow = getRowNumber();
if (row == currentRow)
{
// Nothing to do.
return true;
}
if (row < currentRow)
{
// We already have visited the target row, so the superclass'
// implementation will suffice.
return super.setRowNumber(row);
}
// Continue advancing until we reach the desired row or run out of
// records.
for (int i = currentRow; i < row; i++)
{
if (!next())
{
return false;
}
}
return true;
}
/**
* Scroll the cursor ahead by the specified number of rows. If the
* number is negative, the cursor is moved backward.
* <p>
* This may put the cursor off the end (either end) of the query's
* results.
* <p>
* Scrolling to a previously visited row (in either direction) is an
* inexpensive operation. Scrolling forward to a new row may be an
* expensive operation, since it may require the retrieval and testing of
* many records.
*
* @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)
{
return setRowNumber(getRowNumber() + rows);
}
/**
* Give the unfiltered results object a chance to respond to the session
* closing event.
*
* @see SimpleResults#sessionClosing()
*/
public void sessionClosing()
{
super.sessionClosing();
unfiltered.sessionClosing();
}
/**
* Clear the internal list of result rows and invoke the underlying
* results object's cleanup method.
*/
public void cleanup()
{
super.cleanup();
unfiltered.cleanup();
}
/**
* Indicate whether the full result set is cached in this object.
*
* @return {@code false} to indicate that it is unknown whether all results have been
* collected.
*/
@Override
public boolean isResultSetCached()
{
return unfiltered.isResultSetCached();
}
}