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