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