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