AdaptiveComponent.java

/*
** Module   : AdaptiveComponent.java
** Abstract : A single-table query component used in an adaptive query
**
** Copyright (c) 2015-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20150801 Refactored from AdaptiveQuery, which previously contained this as an inner
**                  class.
** 002 ECF 20160610 Added implementation of isIdOnly to override parent's implementation. Added
**                  missing file header and missing javadoc.
** 003 ECF 20160615 Rolled back isIdOnly change in #002.
** 004 OM  20160629 HQLPreprocessor needs the list of fields of the current index.
** 005 ECF 20160720 Fixed buffer lookup during HQL preprocessing.
** 006 OM  20171212 HQLPreprocessor.get() signature change.
** 007 OM  20180625 Added support for dynamic sorting.
** 008 OM  20180901 Removed unnecessary cast.
** 009 ECF 20200906 New ORM implementation.
**     OM  20220704 getSortCriteria() returns updated set of SortCriterion.
**     DDF 20221114 Changed class to public in order to use it in AdaptiveQueryTraceAspect
**     OM  20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
**     CA  20221130 Refactored the WHERE clause translation (when the bound and definition buffers are not the
**                  same), to be aware of the external buffers (i.e. added for CAN-FIND sub-select clauses).  
**                  The translate will be performed before the query is being executed.
** 010 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 011 RAA 20231018 Added dynamicSortMode to check if the component is currently using dynamic sort.
** 012 RAA 20231220 Skipped variable assignment in initialize.
** 013 AL2 20240318 Added isDirty.
** 014 TJD 20230724 Renaming ORM FQL related functions
*/

/*
** 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.dirty.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;

/**
 * A query component implementation which extends {@link QueryComponent} to store additional
 * information needed when reverting an adaptive query from preselect to dynamic query mode.
 */
public class AdaptiveComponent
extends QueryComponent
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(AdaptiveComponent.class);
   
   /** 
    * HQL sort clause associated with this query component, original. This is parsed at runtime 
    * and used to create the {@code sortCriteria} list.
    */
   private String originalSort;
   
   /**
    * Copy of the original HQL sort clause associated with this query component. This temporary
    * copy of original static sort criteria ({@code originalSort}) is only valid while dynamic
    * sorting criteria are in effect. It is not directly accessed otherwise except when the 
    * dynamic sorting is reverted, the original {@code originalSort} is restored.
    */
   private String originalStaticSort;
   
   /** DMO to which this component should join via a foreign relation. */
   private final DataModelObject inverse;
   
   /** Sort criteria associated with this query component. */
   private List<SortCriterion> sortCriteria = null;
   
   /**
    *  Copy of the original sort criteria associated with this query component.  This temporary
    *  copy of original static sort criteria ({@code sortCriteria}) is only valid while dynamic
    *  sorting criteria are in effect. It is not directly accessed otherwise except when the 
    *  dynamic sorting is reverted, the original {@code sortCriteria} is restored.
    */
   private List<SortCriterion> staticSortCriteria = null;
   
   /** DMO sorter associated with this component's sort clause */
   private DMOSorter dmoSorter = null;
   
   /** FQL preprocessor used for dynamic mode */
   private FQLPreprocessor dynamicFQLPreprocessor;
   
   /** Substitution arguments for this component when in dynamic mode */
   private Object[] dynamicArgs = null;
   
   /** Compound query component fallback for multi-table, dynamic operation */
   private CompoundComponent fallback = null;
   
   /** Event registration ID used to gather index modification events */
   private Long globalEventID = null;
   
   /** ID of last index modification event retrieved from clearinghouse */
   private long lastGlobalEventID = -1L;
   
   /** Flag denoting whether this component is already in dynamic sort mode or not. */
   private boolean dynamicSortMode = false;
   
   /**
    * Constructor.
    * 
    * @param   query
    *          Enclosing adaptive query.
    * @param   proxy
    *          Reference to record buffer updated by this component of the query.
    * @param   join
    *          Helper object for a join via a foreign key relation.  May be {@code null}.
    * @param   where
    *          Restriction criteria in HQL where clause format.
    * @param   translateWhere
    *          A function to translate the where clause, just before it gets executed.
    * @param   sort
    *          Sort criteria in HQL order by clause format.
    * @param   indexInfo
    *          Index information string as it is returned by INDEX-INFORMATION. May be {@code
    *          null} if it is not an OPEN QUERY case or if you don't need debug information about
    *          selected indexes.
    * @param   lockType
    *          Requested record lock type.
    * @param   args
    *          Default (unresolved) query substitution arguments.
    * @param   iteration
    *          Type of iteration:  FIRST, LAST, NEXT, PREVIOUS, or UNIQUE.
    * @param   outer
    *          {@code true} if this component is joined with the previous component via an outer
    *          join;  {@code false} if there is no previous component, or if this component is
    *          joined with the previous component via an inner join.
    * @param   inverse
    *          DMO to which this query should join via a foreign relation. May be {@code null}.
    */
   AdaptiveComponent(AdaptiveQuery query,
                     BufferReference proxy,
                     AbstractJoin join,
                     String where,
                     Supplier<String> translateWhere,
                     String sort,
                     String indexInfo,
                     LockType lockType,
                     Object[] args,
                     int iteration,
                     boolean outer,
                     BufferReference inverse)
   {
      super(query, proxy.buffer(), join, where, translateWhere, indexInfo, lockType, args, iteration, outer);
      
      this.originalSort = sort;
      this.inverse = inverse;
   }
   
   /**
    * Get the FQL preprocessor associated with this component. This may return different
    * preprocessors, depending upon whether the query is currently in preselect vs. dynamic mode.
    * 
    * @return  FQL preprocessor.
    */
   protected FQLPreprocessor getFQLPreprocessor()
   throws PersistenceException
   {
      FQLPreprocessor preselectPreproc = super.getFQLPreprocessor();
      
      return (getEnclosingQuery().isPreselect() || fallback == null
              ? preselectPreproc
              : dynamicFQLPreprocessor);
   }
   
   /**
    * Create the <code>FQLPreprocessor</code> which backs this component.
    * <p>
    * This method overrides the parent's implementation to also create a secondary HQL
    * preprocessor for use in dynamic fetch mode. This secondary version does not inline any
    * substitution parameters. If the primary version did not inline any such parameters, then
    * both the primary and secondary preprocessors will be the same object.
    * 
    * @param   queryArgs
    *          Query substitution parameters used in the preprocessor's creation.
    * 
    * @return  Primary FQL preprocessor (i.e., the version appropriate for use with the enclosing
    *          query's preselect mode).
    * 
    * @throws  PersistenceException
    *          if there is an error creating the preprocessor.
    */
   protected FQLPreprocessor prepareFQLPreprocessor(Object[] queryArgs)
   throws PersistenceException
   {
      FQLPreprocessor preselectPreproc = super.prepareFQLPreprocessor(queryArgs);
      
      if (fallback == null)
      {
         // If HQL preprocessor was inlined for preselect use, we need a
         // non-inlined version for dynamic use.
         if (preselectPreproc.wasInlined())
         {
            RecordBuffer buffer = getBuffer();
            String where = getOriginalWhere();
            Joinable query = getEnclosingQuery();
            dynamicFQLPreprocessor = FQLPreprocessor.get(where,
                                                         buffer.getDatabase(),
                                                         buffer.getDialect(),
                                                         getReferencedBuffers(),
                                                         queryArgs,
                                                         null,
                                                         false,
                                                         null,
                                                         null,
                                                         false,
                                                         null);
         }
         else
         {
            dynamicFQLPreprocessor = preselectPreproc;
         }
      }
      
      return preselectPreproc;
   }
   
   /**
    * Get substitution arguments for this component, resolving them first if necessary. This may
    * return different arguments, depending upon whether the query is in preselect or dynamic
    * mode.
    *
    * @return  Substitution arguments.
    */
   protected Object[] getArgs()
   throws PersistenceException
   {
      Object[] filteredArgs;
      
      if (getEnclosingQuery().isPreselect() || fallback == null)
      {
         filteredArgs = super.getArgs();
      }
      else
      {
         if (dynamicArgs == null && getRawArgs() != null)
         {
            if (super.getFQLPreprocessor().wasInlined())
            {
               dynamicArgs = filterArgs(dynamicFQLPreprocessor);
            }
            else
            {
               dynamicArgs = super.getArgs();
            }
         }
         
         filteredArgs = dynamicArgs;
      }
      
      return filteredArgs;
   }
   
   /**
    * Reset both the preselect and dynamic HQL preprocessors, so that they need to be recreated
    * the next time one is requested.
    */
   protected void resetFQLPreprocessor()
   {
      super.resetFQLPreprocessor();
      
      dynamicFQLPreprocessor = null;
   }
   
   /**
    * Initialize instance variables which could not be set at construction, because they
    * required a scope to first be opened on the backing buffer.
    * 
    * @throws  PersistenceException
    *          if there is a database or parsing error.
    */
   void initialize()
   throws PersistenceException
   {
      if (sortCriteria != null)
      {
         return;
      }
      
      RecordBuffer buffer = getBuffer();
      
      buffer.initialize();
      
      sortCriteria = Collections.unmodifiableList(SortCriterion.parse(originalSort, buffer));
      
      setSortIndex(SortIndex.get(originalSort, buffer));
      
      dmoSorter = buffer.getIndexHelper().getSorterForSortPhrase(buffer, originalSort);
   }
   
   /**
    * Get the fallback compound component, which is used in the event an optimized, compound,
    * adaptive query if forced from preselect mode to dynamic mode.
    * 
    * @return  Compound query fallback component.
    */
   CompoundComponent getFallback()
   {
      return fallback;
   }
   
   /**
    * Set the fallback compound component, which is used in the event an optimized, compound,
    * adaptive query if forced from preselect mode to dynamic mode.
    * 
    * @param   fallback
    *          Compound query fallback component.
    */
   void setFallback(CompoundComponent fallback)
   {
      this.fallback = fallback;
   }
   
   /**
    * Collect and process global events which describe changes made to DMO objects in other
    * sessions. The processing of such events may cause the containing query's current results
    * to be invalidated.
    */
   void processGlobalEvents()
   {
      AdaptiveQuery query = (AdaptiveQuery) getEnclosingQuery();
      
      // Register to receive events if necessary.
      if (globalEventID == null)
      {
         registerForGlobalEvents();
         
         // TODO: extract GlobalEventManager from DirtyShareContext, then remove the following block
         if (globalEventID == null)
         {
            return;
         }
      }
      
      // Gather and process events.
      try
      {
         DirtyShareContext dirtyContext = getBuffer().getDirtyContext();
         if (dirtyContext == null)
         {
            return;
         }
         
         GlobalChangeEvent[] events = dirtyContext.getGlobalEvents(globalEventID, lastGlobalEventID);
         if (events == null)
         {
            return;
         }
         
         int len = events.length;
         if (len > 0)
         {
            lastGlobalEventID = events[len - 1].getEventID();
         }
         
         for (int i = 0; i < len && query.isPreselect(); i++)
         {
            query.stateChanged(events[i]);
         }
      }
      catch (EventRegistrationException exc)
      {
         query.invalidate(this);
         registerForGlobalEvents();
      }
      catch (PersistenceException exc)
      {
         ErrorManager.recordOrThrowError(exc);
      }
   }
   
   /**
    * Collect and process global events which describe changes made to DMO objects in other
    * sessions. The processing of such events may cause the containing query's current results
    * to be invalidated.
    * 
    * @param   localOnly
    *          Check if the component is dirty only in the current dirty context. 
    * 
    * @return   {@code true} if this component operated over a table with dirty changes.
    */
   boolean isDirty(boolean localOnly)
   {
      DirtyShareContext dirtyContext = getBuffer().getDirtyContext();
      return dirtyContext != null && dirtyContext.isEntityDirty(getBuffer().getEntityName(), localOnly);
   }
   
   /**
    * Get the list of sort criteria associated with this query component.
    *
    * @return  An unmodifiable list of sort components.
    */
   List<SortCriterion> getSortCriteria()
   {
      if (sortCriteria == null || sortCriteria.isEmpty())
      {
         return sortCriteria;
      }
      
      List<SortCriterion> ret = new ArrayList<>(sortCriteria.size());
      String alias = getBuffer().getDMOAlias();
      for (SortCriterion criterion : sortCriteria)
      {
         ret.add(criterion.getAlias().equals(alias) ? criterion : new SortCriterion(criterion, alias));
      }
      
      return ret;
   }
   
   /**
    * Get sort criteria in HQL order by clause format for this component.
    *
    * @return  HQL order by clause, before being rewritten by the {@code SortCriterion} class.
    */
   protected String getOriginalSort()
   {
      return originalSort;
   }
   
   /**
    * Get DMO (proxy) to which this component should join via a foreign relation.
    *
    * @return  Inverse DMO proxy or <code>null</code> if no such join is defined for this query.
    */
   DataModelObject getInverse()
   {
      return inverse;
   }
   
   /**
    * Get the DMO sorter associated with this the index used to sort for this query component.
    * 
    * @return  DMO sorter associated with this component's index.
    */
   DMOSorter getDMOSorter()
   {
      return dmoSorter;
   }
   
   /**
    * Clean up when this component is no longer in use.  This involves deregistering for global
    * events, if the component was registered for the same.
    */
   void cleanup()
   {
      if (globalEventID != null)
      {
         DirtyShareContext dirtyContext = getBuffer().getDirtyContext();
         if (dirtyContext != null)
         {
            dirtyContext.deregisterForGlobalEvents(globalEventID);
         }
      }
   }
   
   /**
    * Register to receive global DMO change events, which may cause the current result set of
    * the containing query to be invalidated.
    */
   private void registerForGlobalEvents()
   {
      DirtyShareContext dirtyContext = getBuffer().getDirtyContext();
      if (dirtyContext == null)
      {
         return;
      }
      
      String entity = getBuffer().getEntityName();
      globalEventID = dirtyContext.registerForGlobalEvents(entity);
      
      // TODO: extract GlobalEventManager from DirtyShareContext, then remove the following block
      if (globalEventID != null)
      {
         lastGlobalEventID = globalEventID;
      }
   }
   
   /**
    * Adds a new sorting criterion as the strongest criterion for sorting the result of this
    * query. The other criteria remain queued, but with lower priority. 
    * <p>
    * Use {@link #clearDynamicSortCriteria} to clean all dynamically added criteria and restore
    * the sorting order of the query to its original form.
    *
    * @param   fr
    *          A reference to field used as new strongest criterion. The current criteria will
    *          be kept, but with lower priority.
    * @param   asc
    *          The direction of sorting for this criterion. Use {@code true} for ascending sort
    *          and {@code false} for descending sorting.
    */
   public void addDynamicSortCriterion(FieldReference fr, boolean asc)
   {
      if (staticSortCriteria == null)
      {
         // make sure the static sort data is backed up, if not already done
         staticSortCriteria = sortCriteria;
         originalStaticSort = originalSort;
      }
      
      try
      {
         if (dynamicSortMode)
         {
            originalSort = fr + (asc ? " asc" : " desc") + 
                           (originalSort.isEmpty() ? "" : ", " + originalSort); 
         }
         else
         {
            dynamicSortMode = true;
            originalSort = fr + (asc ? " asc" : " desc");
         }
         
         sortCriteria = Collections.unmodifiableList(
               SortCriterion.parse(originalSort, getBuffer()));
      }
      catch (PersistenceException e)
      {
         LOG.severe("", e);
         // should not be thrown: the buffer is already checked
      }
   }
   
   /**
    * Clears all dynamically added criteria.
    */
   public void clearDynamicSortCriteria()
   {
      dynamicSortMode = false;
      if (staticSortCriteria == null)
      {
         return; // no-op: no criterion was added
      }
      
      sortCriteria = staticSortCriteria;
      staticSortCriteria = null;
      originalSort = originalStaticSort;
      originalStaticSort = null;
   }
}