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