AdaptiveQuery.java
/*
** Module : AdaptiveQuery.java
** Abstract : Query which adapts its retrieval type from preselect to dynamic
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060721 @28133 Created initial version. Query which adapts
** its retrieval type from preselect to dynamic,
** based upon DMO state change notifications.
** 002 ECF 20060725 @28186 Integrated ProgressiveResults. Implementation
** provides better performance for use cases
** which require access to a small number of
** result records while retaining most of the
** preselect performance advantage for larger
** queries.
** 003 ECF 20060730 @28262 Fixed order by clause defect. Order by clause
** was being omitted.
** 004 ECF 20060801 @28277 Fixed NPE in order by processing. Added
** safety code to assembleOrderByClause().
** 005 ECF 20060802 @28347 Enabled transition from dynamic to preselect
** mode. Once invalidated, an AdaptiveQuery will
** now transition back to preselect mode as soon
** as possible.
** 006 ECF 20060803 @28387 Integrated ResultsAdapter and made this class
** more easily extended. Refactored much of the
** behavior into polymorphic methods.
** 007 ECF 20060808 @28573 Refined ResultsAdapter to better handle
** FilteredResults. Integrated ResultsProvider
** interface with new inner class ResultsSource.
** 008 ECF 20060830 @28992 Fixed join behavior. For a single table query
** and for the first table of a multi-table
** query, a natural join's inverse DMO is
** assumed to be provided as a query parameter,
** and a DynamicJoin is created. For all other
** natural joins, the ServerJoin implementation
** is used.
** 009 GES 20060830 @29018 Fixed null ptr exception in tracing code.
** 010 ECF 20060831 @29082 Added support for sorting by an indexed
** property. Specifically, support sorting by
** a property of a DMO's associated list of
** composite properties.
** 011 ECF 20060901 @29154 Fixed NPE. Cannot safely use invalidBuffer to
** extract DMO proxy in createSimpleQuery().
** 012 ECF 20060911 @29459 Fixed defect in transition from dynamic to
** preselect mode. A state change event during
** this transition must cancel the transition.
** 013 ECF 20061006 @30195 Implemented scrolling mode. When set to
** scrolling mode, non-linear scrolling is
** possible in preselect mode. Previously, only
** linear scrolling from first to last was
** possible. This is necessitated by the fact
** that AdaptiveQuery may back a browse widget.
** 014 ECF 20061019 @30524 Additional fixes to support scrolling mode.
** These were made primarily to support the use
** of this query with a Browse widget, and
** possibly as a component within a scrolling
** CompoundQuery.
** 015 ECF 20061027 @30748 Replaced Object with DataModelObject for DMO
** arguments to methods/constructors. Required
** for compile-time type safety.
** 016 ECF 20061119 @31323 Refined invalidation strategy. Do not switch
** from preselect mode on session closing events
** if a reposition listener is registered to the
** query (indicates a browse association). Upon
** record change events, only invalidate if
** changed property is one which would affect
** content or sorting of result set. Also
** improved dynamic mode reposition fetching.
** 017 ECF 20061121 @31343 Added safety code to prevent NPE. During
** setupInvalidationProperties(), query
** component may return null map of restriction
** property names if where clause was null.
** 018 ECF 20061129 @31477 Additional change related to #016 (@31323).
** If associated with a browse on session close,
** discard current high level results object.
** 019 ECF 20061203 @31573 Further changes to sessionClosing() and
** invalidate(RecordBuffer) methods. We now
** invalidate results in the event of an error
** on session close. We also use the explicit
** browse check (isBrowsed()) to detect whether
** the query is associated with a browse when
** invalidating results. Also, this test was
** moved from sessionClosing() to invalidate(),
** to ensure that a browse association was being
** considered in all invalidation scenarios, not
** just session close.
** 020 ECF 20061207 @31617 Support insert RecordChangeEvent. Modified
** stateChanged() method.
** 021 ECF 20070223 @32226 Do not switch to dynamic mode if the record
** currently being visited is deleted. This
** supports a common idiom of deleting records
** in a loop without thrashing back and forth
** between modes.
** 022 ECF 20070227 @32242 Fixed #021 (@32226). The implementation of
** this optimization was incorrect.
** 023 ECF 20070330 @32242 Fixed NPE in stateChanged() method.
** 024 ECF 20070412 @32974 Fixed createSimpleQuery(). Always use lock
** type of NONE for the dynamic query. This will
** be overridden by PreselectQuery.fetch(), if
** a stricter lock is required.
** 025 ECF 20070416 @33017 Integrated user interrupt handling.
** 026 ECF 20070509 @33452 Modified sessionClosing() behavior to behave
** as superclass if scrolling. Temporarily
** rolled back #021 (@32226), which causes
** ProgressiveResults to lose records. Need a
** better solution for this problem which
** preserves the optimization in #021.
** 027 EVL 20070609 @34005 Adding explicit import of the class
** org.hibernate.type.Type to eliminate conflict
** with the same class from java.lang.reflect
** package to be able to compile for Java 6.
** 028 ECF 20070706 @34400 Minor optimization. Replaced StringBuffer
** with StringBuilder.
** 029 ECF 20070709 @34421 Modified sessionClosing(). Always preserve
** preselected results rather than invalidating
** the query. This significantly improves
** performance in certain use patterns where the
** session is closed regularly before the query
** has iterated all of its results (e.g., a
** commit which occurs within a loop at a lower
** nested scope).
** 030 ECF 20070711 @34445 Further improved sessionClosing(). Event
** handling on session close is now delegated to
** the results adapter.
** 031 ECF 20070720 @34905 Added toString() method for debugging.
** 032 ECF 20070827 @34998 Handle temp table multiplex ID when used in
** query's sort criteria. Certain logic had an
** improper dependency on all SortCriterion
** instances having a getter method, which is
** not the case for multiplex IDs.
** 033 ECF 20071022 @35557 Prevent thrashing between modes. Added crude
** safety mechanism which disables transitioning
** from dynamic to preselect mode more than 3
** times.
** 034 ECF 20071130 @36118 Integrated generics.
** 035 ECF 20071206 @36270 Removed sort field from AdaptiveComponent.
** This field was not being used. Constructor
** now sets SortIndex in superclass.
** 036 ECF 20080122 @36888 Fixed handling of state change events in
** dynamic mode. It is necessary to reset the
** dynamic query's cursor to avoid retrieving
** stale data.
** 037 SVL 20071230 @36222 When ProgressiveResults reports that changes
** in results detected while getting them, the
** mode is switched from preselect to dynamic.
** 038 SVL 20080418 @38040 Query registers as ChangeBroker listener in
** the constructor.
** 039 ECF 20080502 @28133 Removed isRepositionOnOpen(). Method is
** obsolete.
** 040 SVL 20080526 @38312 isPrelockUsed() added.
** 041 SVL 20080604 @38535 currentRowImpl() is used instead of
** currentRow().
** 042 CA 20080617 @38867 If there are components with no inverse
** record to join on, then the query will not
** be executed and no results will be returned.
** 043 ECF 20080701 @39033 Optimized stateChanged(). Avoid invalidating
** preselected results in certain delete and
** update cases when we can detect invalidation
** is unnecessary.
** 044 ECF 20080708 @39081 Refined #043 (@39033) optimization. A delete
** of the first record found by the query will
** trigger an immediate query/caching of all
** remaining results. Tightened conditions where
** double-checking of invalidation is done.
** 045 ECF 20080709 @39085 Fixed sessionClosing() and stateChanged(). We
** now prevent using a dead connection after the
** session closes and avoid caching results on a
** delete event in dynamic mode, respectively.
** 046 ECF 20080710 @39091 Modified sessionClosing(). cacheResults()
** no longer throws IllegalStateException.
** 047 ECF 20080723 @39174 Added retry handling. Implement Finalizable
** and prepare for retry in retry() method by
** setting state as if a reposition to the
** current record has occurred.
** 048 CA 20080729 @39219 Removed retry handling. Retry was added back
** to PreselectQuery.
** 049 SVL 20080730 @39223 Support API change in IndexHelper.
** 050 SVL 20080815 @39400 Into provideNewResults() before we skip the
** first result got after switching to the
** preselect mode, we check whether it equals
** the last result retrieved in dynamic mode.
** 051 ECF 20080813 @39553 Support invalidation of results based on DMO
** change events in other sessions. Currently,
** this support is limited to changes which
** affect an index on the backing table.
** 052 CA 20080815 @39449 Support API change in DBUtils.
** 053 ECF 20081014 @40088 Added support for inlining query substitution
** parameters. Integrated HQLPreprocessor API
** changes; modified AdaptiveComponent to use
** appropriate HQL, based on preselect vs.
** dynamic mode.
** 054 ECF 20081030 @40305 Fixed regression introduced by #053 (@40088).
** Refactored AdaptiveComponent inner class and
** code in top-level class which uses it.
** 055 ECF 20081105 @40309 Overrode parent's open() method. Registers
** with the ChangeBroker at the appropriate
** scope.
** 056 CA 20081210 @40836 Do not cleanup if the query was explicitly
** closed.
** 057 CA 20081211 @40858 Fixed regression introduced by H056: on
** sessionClosing event, if the query is closed,
** return true.
** 058 ECF 20081216 @40916 Removed flushBuffers() invocation. This work
** is now done when a component is added to the
** query. The previous invocation, during record
** retrieval, was too late.
** 059 SVL 20090118 @41169 getOriginalWhere() is used together with
** getRawArgs() during dynamic query creation.
** 060 ECF 20090202 @41247 Rolled back #040 (@38312). Prelock mechanism
** has been disabled.
** 061 ECF 20090220 @41350 Override parent's getRow() method. Needed in
** cases where row data is required during a
** transition from preselect to dynamic mode,
** before a new Results object is available.
** 062 ECF 20090630 @42986 Fixed reposition support in conjunction with
** changes to the base class.
** 063 ECF 20090701 @43008 Fixed NPE in setupInvalidationProperties(). It is
** possible for a component to have null sort
** criteria, if the query's only sort is by primary
** key.
** 064 ECF 20090702 @43033 Added isNativelyScrolling(). Overrides parent's
** implementation to report false.
** 065 ECF 20090703 @43053 Reimplemented load() method. This method now sets
** the backing buffer to unknown mode and throws
** MissingRecordException if a target record cannot
** be loaded.
** 066 ECF 20090707 @43102 Fixed regression in #062. When storing results in
** the ResultsAdapter in createResults(), set the
** betweenRows flag to false to ensure the next call
** to next() must advance the query.
** 067 ECF 20090716 @43212 Added late initialization of backing record
** buffer.
** 068 ECF 20090724 @43414 Added implicit transaction support. Added
** push/pop implicit transaction calls to
** DynamicResults.
** 069 CA 20090731 @43464 Register the query for off-end listener
** registration after block initialization (done
** only for block types FOR and FOR EACH).
** 070 ECF 20090810 @43585 Changed to accommodate SessionListener API
** change. sessionClosing() method was replaced with
** sessionEvent().
** 071 ECF 20090811 @43597 Fixed resource cleanup. Made changes to execute()
** to make sure a resource cleaner is registered in
** dynamic mode. Added call to cleanup adapter when
** switching modes.
** 072 SVL 20091030 @44303 Added isNativelyPreselect() function.
** 073 SVL 20120828 Make dynamic results non-ID-only if there is a
** client-side where clause.
** 074 ECF 20131028 Import change.
** 075 SVL 20140210 Use HQLExpression instead of HQL string.
** 076 SVL 20140604 Added support for INDEX-INFORMATION.
** 077 SVL 20140812 Fixed PREV mistakenly treated as LAST.
** 078 SVL 20140909 Added cursor for proper support of scrolling queries.
** Implemented deleteResultListEntry.
** 079 SVL 20141209 Fixed case of full and empty cursor set.
** 080 SVL 20150212 Use normal invalidation for browsed queries. Removed duplicate
** functionality in repositionByID (it is handled by Cursor).
** 081 SVL 20150331 Added deleteResultListEntry(boolean).
** 082 OM 20150507 Removed listener from global scope of ChangeBroker.
** 083 ECF 20150801 Added support for multiple table joins, in the context of an
** optimized compound query. Refactored inner class AdaptiveQuery into
** a top-level class.
** 084 ECF 20150929 Deregister as a SessionListener if a query has been closed.
** 085 EVL 20160217 Clean up comments from symbols invalid for Solaris to compile.
** 086 SVL 20160314 Reference row was added for scrolling queries.
** 087 ECF 20160321 Minor doc fixes.
** 088 ECF 20160515 Doc and format cleanup.
** 089 ECF 20160610 Enable ProgressiveResults to work more effectively with the
** Hibernate query cache when processing a projection query.
** 090 SVL 20160608 The query can handle null IDs (for scrolling compound queries
** with OUTER join).
** 091 OM 20160603 Fixed rtrimming to space character only.
** 092 SVL 20160629 Reset cursor on query (re)open.
** 093 ECF 20160614 Rolled back #089. Track entities associated with query to enable
** smarter flushing strategy for query.
** 094 SVL 20160721 Proper reset on query reopen.
** 095 OM 20160720 Method getPersistence().popImplicitTransaction() changed signature.
** 096 GES 20160804 Switched from WhereExpression to lambda usage.
** 20160919 All initialization is now done with a default constructor followed
** by a call to initialize(). This enables query instance members in
** converted code to be local variables that are effectively final.
** This is needed to support lambdas without moving queries to be
** instance members. Queries as instance members breaks recursion
** use cases.
** 097 CA 20161012 Fixed scope processing when the query is from a persistent
** procedure, and is part of a QUERY resource.
** 098 OM 20161215 Kept breakValue after switching back to preselect mode.
** 099 OM 20170209 Added return value for repositioning methods.
** 100 ECF 20180201 RandomAccessQuery.initializeBuffer has been removed.
** 101 ECF 20180418 Set external buffers into dynamic query for single-component
** adaptive query when switching to dynamic mode.
** 102 ECF 20180709 Inform ProgressiveResults if this is a projection query, to enable
** downstream optimizations.
** 103 OM 20180625 Added support for dynamic sorting criteria.
** 104 ECF 20190302 Renamed setExternalBuffers to addExternalBuffers.
** 105 OM 20190330 Ignore close request when query not fully initialized.
** 106 SVL 20190320 Added createResultListEntry.
** 107 ECF 20190427 Fixed results caching.
** 108 CA 20190811 A dynamic query can be closed and then re-opened.
** EVL 20190822 The closed query should return Unknown value for size request, not 0.
** HC 20190823 Fixed an NPE condition.
** EVL 20190823 Clean up from debug comments.
** 109 SVL 20191002 Misc fixes for the cases exposed during implementation of
** create/delete-reslut-list-entry
** 110 CA 20191005 A query must know if it is dynamic, when closing.
** 111 CA 20191009 Added where and sort clause translation in case of bound buffers.
** 112 SVL 20191218 Handled case when REPOSITION TO ROWID fails.
** 113 ECF 20200215 New ORM implementation.
** 114 CA 20200924 Replaced DMO property access from reflection to property's data handler.
** OM 20201001 Dropped redundant ORDER BY elements in multi-table queries.
** CA 20201005 Fixed assembleOrderByClause - emit the _multiplex column, too, so that H2 will
** match the WHERE and ORDER BY indexes (if possible).
** ECF 20201011 Do not use ProgressiveResults if the sort phrase does not match a legacy index,
** to forego the performance penalty of re-sorting the results for multiple
** brackets.
** 20201019 Modified sort phrase index comparison to only check outermost query component.
** OM 20210226 Fixed navigation of resultsets which include OUTER-JOIN-ed tables.
** CA 20210310 A dynamic predicate must set the default lock to NONE, instead of SHARE.
** SVL 20210413 Added NPE protection in add/clearDynamicSortCriterion.
** ECF 20210723 Changed signature of query close worker method.
** AL2 20210909 Added support for indexed reposition.
** CA 20220117 Fixed close for dynamic PreselectQuery's, the close(boolean) needs to be
** implemented.
** OM 20220125 If the first row is deleted, the cursor is reset and first row is refetched.
** RV 20220526 Improved reset method so it clears breakValue and foundFirst properties.
** OM 20220701 Dropped overriding methods which were duplicating code from super class.
** OM 20220707 Shared meta-knowledge about fields and properties of a record was collected in
** DmoMeta to avoid duplicating their collection and storage.
** OM 20220726 Added recordAlreadyFound flag for block next/prev iterations on unique
** find-by-rowid queries.
** CA 20220901 Refactored scope notification support: ScopeableFactory was removed, and the
** registration is now specific to each type of scopeable. For each case, the block
** will be registered for scope support (for that particular scopeable) only when
** the scopeable is 'active' (i.e. unnamed streams or accumulators are used). This
** allows a lazy registration of scopeables, to avoid the unnecessary overhead of
** processing all the scopeables for each and every block.
** CA 20220531 Ensure the sortTranslated flag is set when the sort is translated with the bound
** buffers.
** 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.
** SVL 20230108 Improved performance by replacing some "for-each" loops with indexed "for" loops.
** SVL 20230109 Reflected PreselectQuery.components() now providing direct access to the
** components array in order to improve performance.
** 115 RAA 20230213 Added the possibility to execute a query in a lazy manner. Also, when a result
** set is created, ProgressiveResults can no longer be the type returned.
** RAA 20230221 If the query will be executed lazy, then it is no longer registered as a
** listener.
** 116 AL2 20230315 Make dynamic results "auto fetch".
** 117 IAS 20230405 Fixed FILL support for recursive DATA-RELATION
** 118 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 119 AL2 20230526 Fixed isLazyMode to handle only single-table queries.
** 120 SVL 20230516 Support for more granular configuration of dirty sharing functionality.
** 121 DDF 20230627 Replaced static variables of DirtyShareSupport with static method calls.
** 122 AB 20230803 Modified assembleWhereClause() to match the new list of arguments, return type
** and to avoid double adding "where" or "and".
** AB 20230822 Modified getCurrentIds(boolean nullable) to take into consideration if
** the component is outer.
** AB 20230830 Modified assembleWhereClause() to append correctly "and" and "where" in
** case of "breakValue".
** 123 SAT 20231002 Added the case when last() is called and the query is attached to a browse.
** 124 AD 20231108 RecordChangeListener code is now executed in PreselectQuery.
** AD 20231113 Added back isLazyMode() check in registerRecordChangeListeners() and
** the unregisterOnCleanup check in cleanup() method.
** AD 20231115 Overridden initialize(String) to fix the registering to record change listener.
** AL2 20231127 Move registerRecordChangeListeners in PreselectQuery.
** 125 ES 20231203 Changed first, last, next, previous, current, unique methods to return boolean,
** instead of void. These methods now returns true if operation was succesful, and false
** not and lenientOffEnd is set on true, otherwise it thrown an QQE exception.
** 126 DDF 20240207 Only cache the results once when TRANSACTION_COMMITTING event is notified.
** DDF 20240208 Only non-scrolling queries will cache results.
** DDF 20240209 Do not set the results when the old and cached results match and if the
** the results are already cached, mark them as such.
** 127 AL2 20240307 Mark the query to retrieve the entire result set (useful for FILL operations).
** 128 AL2 20240318 Invalidate query if there are dirty changes made by this session on a component.
** AL2 20240319 Made the invalidation due to dirty component only at initialization.
** 129 AL2 20240321 Made earlyInvalidate protected.
** 130 CA 20240320 Avoid BDTs within internal FWD runtime. Replaced iterators with Java 'for',
** where it applies.
** 131 CA 20240323 Fixed double-offend registration in 'AdaptiveQuery.initialize()'.
** Query's 'hintFullResults' can not work with forward-only queries.
** CA 20240331 Use ArrayList and Java for loops in query assembler.
** 132 CA 20240402 Translate the where clause before creating the query in createSimpleQuery or
** createCompoundQuery.
** 133 CA 20240422 Allow the query to be reset to its initialization state, so the same instance can
** be used to execute child buffer FILL operations.
** 134 AL2 20240424 Let earlyInvalidate return true if it actually invalidated or false otherwise.
** Let local invalidation for multi-component. Early invalidate when resetting.
** 135 AL2 20240508 Set lenient-off-end to queries that back the dynamic results.
** 136 AL2 20240514 Set OFF-END to FRONT when early invalidating.
** 137 TJD 20240412 Renamed isEnabledCrossSection to isEnabledCrossSession
** TJD 20240730 Allow offEnd.FRONT to be treated like offEnd.BACK for prev/last queries
** 138 AL2 20240906 Implemented get(forceOnlyId). Cached results avoid spoiling the session cache.
*** AL2 20241004 Fixed results get to properly honor the forceOnlyId flag.
** 139 DDF 20230712 Added an additional condition when creating ScrollableResults, the query
** must not have forward-only enabled.
** DDF 20230717 Only allow scrolling when forwardOnly is false.
** DDF 20230721 Added setForwardOnly() method.
** DDF 20230825 Refactor TRANSACTION_COMMITTING event in sessionEvent() method.
** DDF 20240130 Set forwardOnly flag when created a RAQ. Do not create a new cursor
** when forwardOnly is set.
** 140 AP 20241009 Reset sortIndex when a query component is invalidated.
** 141 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** Implementation of [setMultiTenantAlternative] method.
** 142 DDF 20241121 Added delegateChanged() for enabling results caching when the ProgressiveResults
** closes it's delegate.
** 143 AOG 20250109 Added afterReposition calls after forward/backward operation is done.
** 144 AL2 20250129 Added quickRevalidation to avoid useless invalidations that may trigger infinite
** dynamic mode for no gain.
** AL2 20250130 Don't quickly revalidate if the invalid component is different from the one that
** already triggered dynamic mode. Reset invalidComponent if quickly revalidated.
** 145 AL2 20250203 Avoided NPE in quickRevalidation caused by SESSION_CLOSING_WITH_ERROR.
** 146 ES 20250210 Don't quickly revalidate if the query is in dynmaic mode.
** 147 AL2 20250219 Extended use of cacheAll on invalidation when we already iterated some records.
** AL2 20240220 Fixed bug when deleting the last record and the cacheAll operation would have
** still set the betweenRows flag, although the query ran off end.
** 148 ES 20250218 Added available parameter to the coreFetch method call.
** 149 AL2 20250226 The extended use of cacheAll should work only with shouldExecuteForward.
** 150 AL2 20250403 Caching results may cause invalidation. In that case, stop caching.
** 151 AL2 20250415 Make reposition if indexed-reposition smarter to reset query if in dynamic mode.
** 152 ES 20250424 Handle StopConditionException through ErrorManager.
** 153 LS 20250502 When creating a new RAQ in 'createSimpleQuery' set the included/excluded fields.
*/
/*
** 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.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;
/**
* An adaptive, iterating query which first attempts to retrieve its records
* in preselect mode, but changes to dynamic record retrieval as necessary.
* <code>AdaptiveQuery</code> balances the high performance of a preselect
* query implementation with the flexibility of a dynamic query
* implementation.
* <p>
* The design point behind this implementation is that in many if not most
* cases, a preselect query plan is the highest performing strategy. It
* leverages the strength of the database server to do the heavy lifting of
* set-oriented data record retrieval. In most use cases, this is the most
* appropriate retrieval strategy. However, to emulate Progress' semantic,
* it is necessary to reflect table updates in a query which has already
* begun iterating through its result set. This dynamic approach is much
* slower in the P2J architecture, since it requires at minimum one, and
* sometimes many more round trips to the server for each record retrieved.
* Therefore it is desirable to avoid dynamic retrieval mode whenever
* possible.
* <p>
* The one performance weakness of the preselect approach is the case where
* client code does not need the full result set acquired by the query. To
* deal with this, <code>AdaptiveQuery</code> uses an instance of {@link
* ProgressiveResults} to fetch progressively larger brackets of result set
* data while in preselect mode. Thus, early results are returned quickly,
* and if more results are needed, they are fetched in increasingly larger
* batches. This amortizes the access cost most effectively for both ends of
* the spectrum: use cases which require only one or a handful of results
* never incur the cost of larger fetches and use cases which require many
* results acquire them with far fewer calls than in dynamic mode.
* <p>
* <code>AdaptiveQuery</code> takes an optimistic approach by operating in
* preselect mode immediately and for as long as possible. It registers as a
* {@link ChangeBroker} listener. If during its preselect mode operation it
* receives notification of a DMO state change which would invalidate its
* current result set, it automatically switches over to dynamic retrieval
* mode. It will remain in dynamic mode for as long as necessary, but will
* switch back into preselect mode at the earliest opportunity.
* <p>
* Once switching from preselect to dynamic mode, the query will operate in
* this mode until the underlying <code>DynamicResults</code> object detects
* that it is possible to switch back to preselect mode. This occurs when
* the <code>DynamicQuery</code> underlying the <code>DynamicResults</code>
* object crosses a <i>sort band</i> boundary. A <i>sort band</i> is the
* grouping defined by the coarsest sort criterion of the query. A boundary
* is crossed when the DMO property defining the sort band changes from one
* retrieved record to the next. The value of this property in the last
* record before the boundary is crossed is termed the <i>break value</i>.
* When the break value for the underlying dynamic query is non-null, this is
* an indication that the sort band boundary has been crossed. At this point,
* it is possible to construct a new preselect mode query which uses all the
* original query criteria, plus the break value, to ensure the query results
* pick up where the dynamically retrieved result set ended.
* <p>
* TODO: Make invalidation check more robust; currently it is too conservative.
* <p>
* TODO: Make revalidation failsafe more sophisticated; currently it locks into dynamic mode
* after <code>MAX_REVALIDATIONS</code> revalidations.
* <p>
* TODO: Make global invalidation setting configurable from the directory.
*/
public class AdaptiveQuery
extends PreselectQuery
implements VirtualResultsListener
{
/** Maximum number of times query can switch from dynamic to preselect mode */
private static final int MAX_REVALIDATIONS = 3;
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(AdaptiveQuery.class.getName());
/** Force invalidation checks across all sessions */
private static final boolean forceGlobalInvalidation = false;
/** Results adapter which can connect to different low-level delegates */
private ResultsAdapter adapter;
/** Implement invalidation checks across all sessions */
private boolean globalInvalidation = forceGlobalInvalidation;
/** Force dynamic mode from the beginning if there is a dirty component */
private boolean localInvalidation = true;
/** High level results object for parent query */
private Results highLevelResults = null;
/** Query component which triggered invalidation */
private AdaptiveComponent invalidComponent = null;
/** Properties whose changes trigger invalidation, keyed by DMO entity */
private Map<String, Set<String>> invalidationProperties = null;
/** Property value which indicates a new sort band in dynamic mode */
private Object breakValue = null;
/** Number of times this query has transitioned back to preselect mode */
private int revalCount = 0;
/** Are results cached due to a previously committed transaction? */
private boolean resultsCached = false;
/** Preselected results should all be cached on next revalidation */
private boolean cacheOnReval = false;
/** Reflects whether an error was detected while getting results */
protected boolean errorCondition = false;
/** Represents cache of the dynamic scrolling query. */
protected Cursor cursor = null;
/** Determines if we have fetched a result row. Only for non-scrolling queries. */
protected boolean foundFirst = false;
/** Flag to check the ability to fetch the records in a lazy manner, without server-side invalidation */
private Boolean lazyMode = null;
/** Flag indicating the entire result set will be retrieved (useful for FILL operations). */
private boolean forceFullResults = false;
/** Flag to indicate that this query should always operated in dynamic mode, once invalidate */
private boolean avoidRevalidation = false;
/**
* Default constructor. Initialization is deferred until <code>initialize()</code> is
* called.
*/
public AdaptiveQuery()
{
}
/**
* Initialization logic which is typically used for a multi-table query. Details
* specific to each table component of the query are added via one of the
* <code>addComponent</code> method variants.
* <p>
* Each component will provide its own sort criteria; they are combined
* when the query is first executed in preselect mode.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @return This query instance.
*/
public AdaptiveQuery initialize()
{
super.initialize(null);
adapter = new ResultsAdapter(new ResultsSource());
TransactionManager.registerOffEndQuery(this);
return this;
}
/**
* Initializer logic which is typically used for a multi-table query. Details
* specific to each table component of the query are added via one of the
* <code>addComponent</code> method variants.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param sort
* FQL order by clause to be applied across all components of the query.
*
* @return This query instance.
*/
public AdaptiveQuery initialize(String sort)
{
this.sort = sort;
components = new ArrayList<>(3);
TransactionManager.registerOffEndQuery(this);
earlyInvalidate();
return this;
}
/**
* Initialization logic which is typically used for a multi-table query. Details
* specific to each table component of the query are added via one of the
* <code>addComponent</code> method variants.
* <p>
* This variant is used by code which is converted from an OPEN QUERY
* statement, which permits advancing off the end of a result set without
* raising a <code>QueryOffEndException</code>.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param lenientOffEnd
* <code>true</code> if advancing past either end of the result
* set should NOT raise a <code>QueryOffEndException</code>;
* <code>false</code> if doing so should raise the exception.
*
* @return This query instance.
*/
public AdaptiveQuery initialize(boolean lenientOffEnd)
{
super.initialize(lenientOffEnd, null);
adapter = new ResultsAdapter(new ResultsSource());
TransactionManager.registerOffEndQuery(this);
earlyInvalidate();
return this;
}
/**
* Initialization logic designed for a single-table adaptive query.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this query. If {@code null}, the default lock level
* is assumed.
*
* @return This query instance.
*/
public AdaptiveQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
DataModelObject inverse,
Object[] args,
LockType lockType)
{
if (lockType == null)
{
lockType = getDefaultLock();
}
String translatedSort = sort;
RecordBuffer defBuffer = ((BufferReference) dmo).definition();
RecordBuffer buffer = ((BufferReference) dmo).buffer();
if (!buffersMatch(buffer, defBuffer))
{
translatedSort = translateSort(buffer, defBuffer, sort);
sortTranslated = true;
}
initialize(translatedSort);
addComponent(dmo, where, sort, indexInfo, inverse, args, lockType, NEXT, false);
adapter = new ResultsAdapter(new ResultsSource());
if (whereExpr != null)
{
addWhereExpression(whereExpr);
}
registerRecordChangeListeners(null);
earlyInvalidate();
return this;
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>no substitution parameters
* <li>share lock
* <li>inner join
* <li><code>NEXT</code> iteration type
* <li>no inverse join DMO
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
*/
public void addComponent(DataModelObject dmo, String where, String sort)
{
addComponent(dmo, where, sort, null, null, getDefaultLock(), NEXT, false);
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>no substitution parameters
* <li>share lock
* <li>inner join
* <li><code>NEXT</code> iteration type
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
DataModelObject inverse)
{
addComponent(dmo, where, sort, inverse, null, getDefaultLock(), NEXT, false);
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>share lock
* <li>inner join
* <li><code>NEXT</code> iteration type
* <li>no inverse join DMO
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
Object[] args)
{
addComponent(dmo, where, sort, null, args, getDefaultLock(), NEXT, false);
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>share lock
* <li>inner join
* <li><code>NEXT</code> iteration type
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
DataModelObject inverse,
Object[] args)
{
addComponent(dmo, where, sort, inverse, args, getDefaultLock(), NEXT, false);
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>no substitution parameters
* <li>inner join
* <li><code>NEXT</code> iteration type
* <li>no inverse join DMO
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param lockType
* Lock type to apply to records retrieved by this component of
* the query.
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
LockType lockType)
{
addComponent(dmo, where, sort, null, null, lockType, NEXT, false);
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>no substitution parameters
* <li>inner join
* <li><code>NEXT</code> iteration type
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param lockType
* Lock type to apply to records retrieved by this component of
* the query.
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
DataModelObject inverse,
LockType lockType)
{
addComponent(dmo, where, sort, inverse, null, lockType, NEXT, false);
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>inner join
* <li><code>NEXT</code> iteration type
* <li>no inverse join DMO
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this component of
* the query.
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
Object[] args,
LockType lockType)
{
addComponent(dmo, where, sort, null, args, lockType, NEXT, false);
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>inner join
* <li><code>NEXT</code> iteration type
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this component of
* the query.
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
DataModelObject inverse,
Object[] args,
LockType lockType)
{
addComponent(dmo, where, sort, inverse, args, lockType, NEXT, false);
}
/**
* Add a component to the query. The following defaults are used:
* <ul>
* <li>no inverse join DMO
* </ul>
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this component of
* the query.
* @param iteration
* Iteration/retrieval mode for this component of the query.
* Only <code>NEXT</code> is supported.
* @param outer
* <code>true</code> if an outer join is required. At this time,
* only inner joins are supported. Future support for outer
* joins is expected.
*
* @throws UnsupportedOperationException
* if <code>outer</code> is <code>true</code> (temporary);
* if <code>iteration</code> is not <code>NEXT</code> (temporary);
* if <code>dmo</code>'s associated database is different than
* that of a previously added component's database (cross-database
* preselect joins are not possible).
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
Object[] args,
LockType lockType,
int iteration,
boolean outer)
{
addComponent(dmo, where, sort, null, args, lockType, iteration, outer);
}
/**
* Add a component to the query.
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this component of
* the query.
* @param iteration
* Iteration/retrieval mode for this component of the query.
* Only <code>NEXT</code> is supported.
* @param outer
* <code>true</code> if an outer join is required. At this time,
* only inner joins are supported. Future support for outer
* joins is expected.
*
* @throws UnsupportedOperationException
* if <code>outer</code> is <code>true</code> (temporary);
* if <code>iteration</code> is not <code>NEXT</code> (temporary);
* if <code>dmo</code>'s associated database is different than
* that of a previously added component's database (cross-database
* preselect joins are not possible).
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
DataModelObject inverse,
Object[] args,
LockType lockType,
int iteration,
boolean outer)
{
addComponent(dmo, where, sort, null, inverse, args, lockType, iteration, outer);
}
/**
* Add a component to the query.
*
* @param dmo
* DMO proxy which defines buffer and record type.
* @param where
* FQL where clause. May be <code>null</code>.
* @param sort
* FQL order by clause.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this component of
* the query.
* @param iteration
* Iteration/retrieval mode for this component of the query.
* Only <code>NEXT</code> is supported.
* @param outer
* <code>true</code> if an outer join is required. At this time,
* only inner joins are supported. Future support for outer
* joins is expected.
*
* @throws UnsupportedOperationException
* if <code>outer</code> is <code>true</code> (temporary);
* if <code>iteration</code> is not <code>NEXT</code> (temporary);
* if <code>dmo</code>'s associated database is different than
* that of a previously added component's database (cross-database
* preselect joins are not possible).
*/
public void addComponent(DataModelObject dmo,
String where,
String sort,
String indexInfo,
DataModelObject inverse,
Object[] args,
LockType lockType,
int iteration,
boolean outer)
{
if (getTableCount() > 0)
{
throw new UnsupportedOperationException(
"Multi-table adaptive queries are not supported in this release");
}
if (iteration != NEXT)
{
throw new UnsupportedOperationException(
"Adaptive query only supports the NEXT iteration type");
}
BufferReference proxy = (BufferReference) dmo;
BufferReference inverseProxy = (BufferReference) inverse;
AbstractJoin join = processJoin(proxy, inverseProxy);
RecordBuffer buffer = proxy.buffer();
RecordBuffer defBuffer = proxy.definition();
Supplier<String> translateWhere = null;
if (!buffersMatch(buffer, defBuffer) || anyBoundBufferInComponent())
{
sort = translateSort(buffer, defBuffer, sort);
sortTranslated = true;
translateWhere = where == null ? null : () ->
{
List<RecordBuffer> binding = new ArrayList<>();
List<RecordBuffer> definition = new ArrayList<>();
binding.add(buffer);
definition.add(defBuffer);
for (int i = 0; i < components.size(); i++)
{
QueryComponent qc = components.get(i);
if (qc.getBuffer() != qc.getDefinitionBuffer())
{
binding.add(qc.getBuffer());
definition.add(qc.getDefinitionBuffer());
}
}
return translateWhere(binding, definition, where);
};
}
AdaptiveComponent comp = new AdaptiveComponent(this,
proxy,
join,
where,
translateWhere,
sort,
indexInfo,
lockType,
args,
iteration,
outer,
inverseProxy);
addComponent(comp);
// Temporary tables are local by nature, so we don't care what
// happens in other sessions.
if (globalInvalidation && comp.getBuffer().isTemporary())
{
globalInvalidation = false;
}
}
/**
* Open the query, first registering with the <code>ChangeBroker</code> at
* the appropriate scope.
*/
public void open()
{
closed = false;
if (cursor != null)
{
cursor.reset();
}
invalidComponent = null;
revalCount = 0;
foundFirst = false;
avoidRevalidation = false;
earlyInvalidate();
super.open();
}
/**
* Check if this type of query should register record change listeners. Use this
* to avoid computing any scope for registration and end up with a no registration
* behavior.
*
* @return {@code true} if we should invest time into computing the scope in order
* to call {@link #registerRecordChangeListeners} or {@code false} if we
* can skip doing any work to honor {@link #registerRecordChangeListeners}.
*/
@Override
public boolean shouldRegisterRecordChangeListeners()
{
return !isLazyMode();
}
/**
* Retrieve the first composite row of results for the query. The
* underlying buffer for each query component is updated with the
* appropriate record.
* <p>
* If the query involves outer joins, some of the underlying buffers may
* be empty when this method returns.
*
* @param lockType
* Lock type to apply to records retrieved (overrides default
* lock type set for each query component).
*
* @return <code>true</code> if query could retrive the next record succefully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if the query or the fetch of any record fails, or if no records
* are available and the query is set to fail in this case.
* @throws QueryOffEndException
* if no records are available and the query is set not to fail
* in this case.
* @throws IllegalStateException
* if no query component has been added to this query.
*/
public boolean first(LockType lockType)
{
if (isScrolling())
{
boolean available = false;
if (cursor.isFoundFirst())
{
Object[] data = cursor.getFirst();
if (data == null && cursor.isFullSet())
{
// The set is full and empty.
loadByValues(null, lockType);
offEnd = OffEnd.BACK;
}
else if (data != null)
{
available = true;
if (!loadByValues(data, lockType))
{
// this can happen when the first record was deleted externally, by other query or directly
// by buffer and this query/cursor is not aware of the situation
cursor.reset();
try
{
available = super.first(lockType);
}
catch (QueryOffEndException e)
{
cursor.addResultFirst(null);
throw e;
}
cursor.addResultFirst(getCurrentIds());
}
}
else
{
if (cursor.isFirstRowDeleted() && cursor.isForwardScroll())
{
return next(lockType);
}
else
{
try
{
available = super.first(lockType);
}
catch (QueryOffEndException e)
{
cursor.addResultFirst(null);
throw e;
}
}
cursor.addResultFirst(getCurrentIds());
}
}
else
{
try
{
available = super.first(lockType);
}
catch (QueryOffEndException e)
{
cursor.addResultFirst(null);
throw e;
}
cursor.addResultFirst(getCurrentIds());
}
return available;
}
else
{
return super.first(lockType);
}
}
/**
* Retrieve the last composite row of results for the query. The
* underlying buffer for each query component is updated with the
* appropriate record.
* <p>
* If the query involves outer joins, some of the underlying buffers may
* be empty when this method returns.
*
* @param lockType
* Lock type to apply to records retrieved (overrides default
* lock type set for each query component).
*
* @return <code>true</code> if query could retrive the next record succefully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if the query or the fetch of any record fails, or if no records
* are available and the query is set to fail in this case.
* @throws QueryOffEndException
* if no records are available and the query is set not to fail
* in this case.
* @throws IllegalStateException
* if no query component has been added to this query.
*/
public boolean last(LockType lockType)
{
if (isScrolling())
{
if (isBrowsed() && !cursor.isFullSet())
{
Results results = getResults();
while (results.next())
{
try
{
fetch(true, lockType, false);
}
catch (MissingRecordException e)
{
continue;
}
cursor.addResultNext(getCurrentIds());
}
// Mark that the set is full.
cursor.addResultNext(null);
// current row is null after results().next and we need to go back to the last row
results.previous();
return true;
}
boolean avail = false;
if (cursor.isFoundFirst())
{
Object[] data = cursor.getLast();
if (data == null && cursor.isFullSet())
{
// The set is full and empty.
loadByValues(null, lockType);
offEnd = OffEnd.BACK;
return false;
}
else if (data != null)
{
avail = true;
if (!loadByValues(data, lockType))
{
avail = previous(lockType);
}
return avail;
}
}
try
{
avail = super.last(lockType);
}
catch (QueryOffEndException e)
{
cursor.addResultLast(null);
throw e;
}
cursor.addResultLast(getCurrentIds());
return avail;
}
else
{
return super.last(lockType);
}
}
/**
* Get the 1-based index of the current row in the result set.
*
* @return Current row or the unknown value if the query has not yet
* executed.
*/
public integer currentRowImpl()
{
if (isScrolling())
{
return cursor.currentRow();
}
else
{
return super.currentRowImpl();
}
}
/**
* Progress-compatible function that returns the 1-based index of the
* current row in the result set.
*
* @return Unknown value if the query is not open or result set is empty.
* 1 if the query is positioned before the first record.
* A value 1 greater than the number of rows in the query result
* set if the query is positioned beyond the last record.
* Otherwise returns the number of rows in the query result set.
*
*/
public integer currentRow()
{
if (isScrolling())
{
return cursor.currentRow();
}
else
{
return super.currentRow();
}
}
/**
* Reload the current composite row of results for the query, overriding
* the lock type to apply to each record.
* <p>
* If the query involves outer joins, some of the underlying buffers may
* be empty when this method returns.
*
* @param lockType
* Lock type which should override that of any query component.
*
* @return {@code true} if the current operation actually reloaded the
* current composite row of results for the query.
*
* @throws ErrorConditionException
* if the reload triggers an error.
* @throws IllegalStateException
* if no query component has been added to this query.
*/
public boolean current(LockType lockType)
{
if (currentRowDeleted)
{
if (isScrolling() || _isOffEnd())
{
ErrorManager.displayError(4114, "No query record is available", false);
}
return false;
}
return super.current(lockType);
}
/**
* Reposition the query to the specified row in the result set. The row
* indicates the position between two results, such that a call to
* retrieve the next record will get the record after the new position,
* and a call to retrieve the previous record will get the record before
* the new position.
*
* @param row
* 1-based index of the target position.
*
* @throws ErrorConditionException
* if there is an error repositioning the cursor.
*/
public void reposition(int row)
{
if (components == null)
{
return;
}
if (isScrolling())
{
prepareReposition();
cursor.reposition(row);
OffEnd res = cursor.getOffEnd();
if (!OffEnd.NONE.equals(res))
{
offEnd = res;
betweenRows = false;
}
else
{
betweenRows = true;
}
afterReposition(true);
}
else
{
super.reposition(row);
}
}
/**
* Advance the current cursor position forward by the specified number of
* rows. This will always leave the cursor between two results. For
* example, given a request to move 1 row forward:
* <ul>
* <li>if the cursor currently is positioned <i>directly on a result</i>,
* this would move it ahead <i>one and a half positions</i>, to just
* beyond the following result;
* <li>if the cursor currently is positioned <i>between two results</i>,
* this would move it ahead <i>one position</i>, to just beyond the
* following result.
* </ul>
*
* @param rows
* Number of rows to scroll the cursor forward.
*
* @return {@code true} if operation was successful and the cursor was moved the exact amount
* of rows as specified by the parameter. If {@code false} is returned then it is
* possible that cursor's position was moved but not with {@code rows} rows.
*
* @throws ErrorConditionException
* if there is an error repositioning the cursor.
*/
public boolean forward(int rows)
{
if (isScrolling())
{
if (rows == 0)
{
return true;
}
if (rows < 0)
{
return backward(-rows);
}
prepareReposition();
boolean moved = cursor.forward(rows);
offEnd = cursor.getOffEnd();
afterReposition(true);
return moved;
}
else
{
return super.forward(rows);
}
}
/**
* Reposition the cursor such that a request to retrieve the next result
* will retrieve the result which matches the specified array of primary
* key IDs. A request to retrieve the previous result will retrieve the
* result immediately previous to this, if any, in the results list.
*
* @param ids
* Array of primary key IDs, arranged from left to right to
* coincide with the records being joined by the underlying
* query.
* @param forceFetch
* <code>true</code> to override the current fetch-on-reposition
* setting to <code>true</code>, in case the underlying
* <code>Results</code> implementation attempts to fetch the
* record during repositioning; <code>false</code> to honor the
* current setting.
*
* @return <code>true</code> if reposition was successful;
* <code>false</code> if unsuccessful.
*
* @throws ErrorConditionException
* if there is an error repositioning the cursor.
*/
protected boolean repositionByID(Long[] ids, boolean forceFetch)
{
return repositionByID(ids, forceFetch, true);
}
/**
* Reposition the cursor such that a request to retrieve the next result
* will retrieve the result which matches the specified array of primary
* key IDs. A request to retrieve the previous result will retrieve the
* result immediately previous to this, if any, in the results list.
*
* @param ids
* Array of primary key IDs, arranged from left to right to
* coincide with the records being joined by the underlying
* query.
* @param forceFetch
* <code>true</code> to override the current fetch-on-reposition
* setting to <code>true</code>, in case the underlying
* <code>Results</code> implementation attempts to fetch the
* record during repositioning; <code>false</code> to honor the
* current setting.
* @param scan
* For scrolling queries: <code>true</code> to scan all results in
* the event a match is not found in the cache; <code>false</code>
* to check the cache only.
*
* @return <code>true</code> if reposition was successful;
* <code>false</code> if unsuccessful.
*
* @throws ErrorConditionException
* if there is an error repositioning the cursor.
*/
protected boolean repositionByID(Long[] ids, boolean forceFetch, boolean scan)
{
if (isScrolling())
{
prepareReposition();
integer oldCurrentRow = scan ? currentRow() : null;
boolean done = false;
try
{
try
{
if (!scan)
{
done = cursor.reposition(ids, false);
}
else if (!indexedReposition)
{
done = cursor.reposition(ids, true);
}
else
{
// indexed-reposition case
if (this.isPreselect())
{
// preselect mode
try
{
done = cursor.reposition(ids, true);
}
catch (QueryOffEndException offEndExc)
{
// no-op
}
if (!done)
{
reset(true, false);
done = cursor.reposition(ids, true);
}
}
else
{
// dynamic mode
if (cursor.reposition(ids, false))
{
// look-up in cache first
done = true;
}
else
{
// the query is in dynamic mode
reset(true, false);
done = cursor.reposition(ids, true);
}
}
}
}
catch (QueryOffEndException offEndExc)
{
throw offEndExc;
}
}
finally
{
offEnd = cursor.getOffEnd();
if (scan)
{
afterReposition(done, oldCurrentRow);
}
else
{
afterReposition(done);
}
}
return done;
}
else
{
return super.repositionByID(ids, forceFetch);
}
}
/**
* Return the number of results found for this query.
* <p>
* Note that the first time this method is invoked, the database server
* may have to scroll to the end of the result set, which may be a
* time-consuming process for a large result set.
*
* @return Size of result set or value if the query is not open.
*/
public integer size()
{
if (isScrolling())
{
return closed ? new integer() : cursor.size();
}
else
{
return super.size();
}
}
/**
* Retrieve the next composite row of results for the query. The
* underlying buffer for each query component is updated with the
* appropriate record.
*
* @param lockType
* Lock type to apply to records retrieved (overrides default
* lock type set for each query component).
*
* @return <code>true</code> if query could retrive the next record succefully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if the query or the fetch of any record fails, or if no records
* are available and the query is set to fail in this case.
* @throws QueryOffEndException
* if no records are available and the query is set not to fail
* in this case.
* @throws IllegalStateException
* if no query component has been added to this query.
*/
public boolean next(LockType lockType)
{
if (isScrolling() && cursor.isFoundFirst())
{
Object[] data;
do
{
data = cursor.getNext();
if (data == null && (cursor.isFullSet() || !cursor.isForwardScroll()))
{
offEnd = OffEnd.BACK;
try
{
fetch(false, lockType, false);
}
catch (MissingRecordException e)
{
// cannot happen, we are not loading a record
}
return false;
}
if (data != null)
{
if (loadByValues(data, lockType))
{
return true;
}
}
} while (data != null);
}
boolean connected = (adapter != null && adapter.isConnected());
invalidateIfReferenceRowExists();
Results results = getResults();
boolean missing;
boolean avail = false;
do
{
missing = false;
try
{
avail = false;
if (betweenRows && connected && !isScrolling())
{
avail = true;
}
else
{
errorCondition = false;
if (!results.next())
{
if (errorCondition)
{
// if an error condition was detected (and handled) then
// we should retry next() action
return next(lockType);
}
else
{
avail = false;
}
}
else
{
avail = true;
}
}
offEnd = (avail ? OffEnd.NONE : OffEnd.BACK);
fetch(avail, lockType, false);
if (isScrolling())
{
if (avail)
{
cursor.addResultNext(getCurrentIds());
}
else
{
cursor.addResultNext(null);
}
}
}
catch (QueryOffEndException exc)
{
if (isScrolling())
{
cursor.addResultNext(null);
}
offEnd = OffEnd.BACK;
if (!isLenientOffEnd())
{
exc.fillInStackTrace();
throw exc;
}
}
catch (MissingRecordException exc)
{
missing = true;
}
finally
{
betweenRows = false;
}
} while (missing);
return avail;
}
/**
* Fetch the array of primary key IDs associated with the next result in
* this query's result list.
*
* @return Array of record IDs reflecting the next query result, or
* <code>null</code> if there is no such result.
*/
public Object[] peekNext()
{
Object[] res = super.peekNext();
if (isScrolling())
{
cursor.addResultNext(res);
}
return res;
}
/**
* Fetch the array of primary key IDs associated with the previous result
* in this query's result list.
*
* @return Array of record IDs reflecting the previous query result, or
* <code>null</code> if there is no such result.
*/
public Object[] peekPrevious()
{
Object[] res = super.peekPrevious();
if (isScrolling())
{
cursor.addResultPrevious(res);
}
return res;
}
/**
* Retrieve the previous composite row of results for the query. The
* underlying buffer for each query component is updated with the
* appropriate record.
*
* @param lockType
* Lock type to apply to records retrieved (overrides default
* lock type set for each query component).
*
* @return <code>true</code> if query could retrive the next record succefully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if the query or the fetch of any record fails, or if no records
* are available and the query is set to fail in this case.
* @throws QueryOffEndException
* if no records are available and the query is set not to fail
* in this case.
* @throws IllegalStateException
* if no query component has been added to this query.
*/
public boolean previous(LockType lockType)
{
if (isScrolling() && cursor.isFoundFirst())
{
Object[] data;
do
{
data = cursor.getPrevious();
if (data == null && (cursor.isFullSet() || cursor.isForwardScroll()))
{
offEnd = OffEnd.FRONT;
try
{
fetch(false, lockType, false);
}
catch (MissingRecordException e)
{
// cannot happen, we are not loading a record
}
return false;
}
if (data != null)
{
if (loadByValues(data, lockType))
{
return true;
}
}
} while (data != null);
}
boolean setToLast = treatPreviousAsLast();
boolean avail = false;
try
{
// When moving backward, all revalidation bets are off.
breakValue = null;
avail = super.previous(lockType);
if (isScrolling())
{
if (!isOffEnd().booleanValue())
{
cursor.addResultPrevious(getCurrentIds());
}
else
{
cursor.addResultPrevious(null);
}
}
return avail;
}
catch (UnsupportedOperationException exc)
{
// We tried to do something the underlying Results implementation
// could not do; invalidate and try again.
if (isPreselect())
{
invalidate();
if (!setToLast)
{
execute();
}
avail = super.previous(lockType);
if (isScrolling())
{
if (!isOffEnd().booleanValue())
{
cursor.addResultPrevious(getCurrentIds());
}
else
{
cursor.addResultPrevious(null);
}
}
return avail;
}
else
{
throw exc;
}
}
}
/**
* Move the current cursor position backward by the specified number of
* rows. This will always leave the cursor between two results. For
* example, given a request to move 1 row backward:
* <ul>
* <li>if the cursor currently is positioned <i>directly on a result</i>,
* this would move it back <i>one half position</i>, to just
* before the current result;
* <li>if the cursor currently is positioned <i>between two results</i>,
* this would move it back <i>one position</i>, to just before the
* current result.
* </ul>
*
* @param rows
* Number of rows to scroll the cursor backward.
*
* @return {@code true} if operation was successful and the cursor was moved the exact amount
* of rows as specified by the parameter. If {@code false} is returned then it is
* possible that cursor's position was moved but not with {@code rows} rows.
*
* @throws ErrorConditionException
* if there is an error repositioning the cursor.
*/
public boolean backward(int rows)
{
if (isScrolling())
{
if (rows == 0)
{
return true;
}
if (rows < 0)
{
return forward(-rows);
}
prepareReposition();
boolean moved = cursor.backward(rows);
offEnd = cursor.getOffEnd();
afterReposition(true);
return moved;
}
else
{
if (rows != 0)
{
// When moving backward, all revalidation bets are off.
breakValue = null;
}
return super.backward(rows);
}
}
/**
* Reset the query by clearing its state to a known default. This is
* used to ensure that subsequent requests for FIRST and LAST record
* retrievals operate on a known query/buffer state.
* <p>
* This implementation nulls out the current result set, so that the next
* record retrieval request triggers a new server-side query. Optionally,
* it also resets resolvable substitution arguments and triggers where
* expression arguments to be resolved, if a client-side where cause
* expression is defined.
* <p>
* For scrolling queries cache is reset.
*
* @param resolveArgs
* <code>true</code> if query substitution arguments should be
* resolved/reset as part of the reset; <code>false</code> if
* existing argument values should be used for the next query
* execution.
* @param forceOrigArgs
* Flag indicating that the original arguments used at the construction of the query must be
* re-calculated.
*/
@Override
public void reset(boolean resolveArgs, boolean forceOrigArgs)
{
super.reset(resolveArgs, forceOrigArgs);
if (isScrolling())
{
cursor.reset();
}
invalidComponent = null;
breakValue = null;
foundFirst = false;
earlyInvalidate();
}
/**
* Load one or more records into their associated buffers, given an array
* of primary key ID values, or the actual DMOs themselves. The IDs/DMOs
* are arranged to coincide with the tables underlying the query, from
* left-most to right-most (in the sense of how they are joined by the
* query).
* <p>
* In the event an expected value cannot be loaded (e.g., the record has
* been deleted or is otherwise no longer available), this method throws
* {@link MissingRecordException}, after setting the associated buffer(s)
* into {@link RecordBuffer#setUnknownMode() unknown mode}. Note that a
* record being locked by another session does not constitute a "missing"
* record. Note also that this exception is not thrown until all the
* elements of <code>data</code> have been processed to the extent
* possible.
*
* @param data
* Array of primary key IDs or DMOs, one per table involved in the
* query.
* @param lockType
* Lock type which should by used. Set to <code>null</code> to
* allow query to use its current lock type.
* @param silentIfNullId
* If <code>true</code>, do not raise {@link MissingRecordException}
* if some of the provided IDs are <code>null</code>. <code>null</code>
* IDs are valid for queries with OUTER join.
*
* @throws MissingRecordException
* if any record cannot be loaded, because it is no longer
* available (e.g., deleted, etc.).
* @throws PersistenceException
* if there is an error loading data.
*/
public void load(Object[] data, LockType lockType, boolean silentIfNullId)
throws MissingRecordException,
PersistenceException
{
if (isScrolling())
{
Long[] arr = new Long[data.length];
if (data.length > 0 && data[0] instanceof Long)
{
System.arraycopy(data, 0, arr, 0, data.length);
}
else
{
for (int i = 0; i < data.length; i++)
{
arr[i] = (data[i] == null) ? null : ((Record) data[i]).primaryKey();
}
}
if (repositionByID(arr, true, false))
{
cursor.next();
offEnd = OffEnd.NONE;
}
}
else if (isPreselect())
{
super.load(data, lockType, silentIfNullId);
}
try
{
coreFetch(data, lockType, data != null, false, silentIfNullId);
}
catch (MissingRecordException exc)
{
setEmptyBuffersUnknown();
throw exc;
}
}
/**
* Assemble an array of primary key IDs for the current record(s) in the
* buffers underlying this query, from left-most to right-most (in the
* sense of how the query joins the associated tables). This method
* essentially takes a lightweight snapshot of the currently loaded
* record(s), so that this may be restored later.
* <p>
* This method overrides the parent's implementation to handle the case
* where we have just transitioned from preselect to dynamic mode, but we
* do not yet have a <code>Results</code> object from which to gather data.
* We generate a dynamic <code>Results</code> object and position to the
* first result, then use the parent's implementation to gather the data.
*
* @return Array of primary key IDs or DMOs (?), one per table involved in the
* query.
*
* @see #load
*/
public Object[] getRow()
{
Object[] srcData = super.getRow();
if (srcData == null && !isPreselect())
{
Results results = getResults();
results.first();
srcData = super.getRow();
}
return srcData;
}
/**
* Assemble an array of primary key IDs for the current record(s) in the buffers underlying
* this query, from left-most to right-most (in the sense of how the query joins the associated
* tables). This method essentially takes a lightweight snapshot of the currently loaded
* record(s), so that this may be restored later.<p>
* For scrolling queries it gets the row from the cache instead of interrogating
* {@code results}. For non-scrolling queries it works in the same way as {@link #getRow()}.
*
* @return Array of primary key IDs, one per table involved in the query.
*
* @see #load(Object[], LockType, boolean)
*/
public Object[] getCachedRow()
{
if (isScrolling())
{
return cursor.getRow();
}
else
{
return getRow();
}
}
/**
* Respond to a dirty (i.e., error-driven) session closing event by
* invalidating any current, preselected results. This will cause a
* switch to dynamic mode, if the query is not associated with a browse.
* If we are in dynamic mode already for a clean session close, this
* method is a no-op.
* <p>
* On a clean (i.e., no error) session closing event, delegate the event
* to the results adapter.
*
* @param event
* Session event type.
*
* @return Under normal conditions, <code>false</code>, indicating this
* query should not be deregistered as a session listener after
* this method returns. Under an error condition, return
* <code>true</code>.
*
* @throws PersistenceException
* not thrown (required by interface).
*/
public boolean sessionEvent(SessionListener.Event event)
throws PersistenceException
{
if (closed)
{
return true;
}
boolean deregister = SessionListener.Event.SESSION_CLOSING_WITH_ERROR.equals(event);
switch (event)
{
case TRANSACTION_COMMITTING:
if (resultsCached || isScrolling())
{
break;
}
if (cacheOnReval || isPreselect())
{
if (adapter.isResultSetCached())
{
resultsCached = true;
break;
}
if (adapter.isConnected())
{
Results old = adapter.getResults();
errorCondition = false;
Results cached = cacheResults(old, true);
if (errorCondition)
{
// caching the results made the query go into dynamic mode
break;
}
else if (old != cached)
{
old.sessionClosing();
adapter.setResults(cached);
}
}
}
else
{
adapter.sessionClosing();
}
resultsCached = true;
break;
case SESSION_CLOSING_NORMALLY:
if (!resultsCached)
{
resetResults();
}
break;
case SESSION_CLOSING_WITH_ERROR:
invalidate(invalidComponent);
break;
default:
return false;
}
return deregister;
}
/**
* Create a string representation of this object's state, primarily for debug purposes.
*
* @return String representation of this object.
*/
public String toString()
{
return "[" + (isPreselect() ? "PRE" : "DYN") + "] " + super.toString();
}
/**
* Indicate whether this query preselects all of its results. This
* implementation initially preselects its results; however, the current
* result set may be invalidated, at which point the query switches to a
* dynamic retrieval mode.
*
* @return <code>true</code> if the query currently is in preselect mode,
* <code>false</code> if its result set has been invalidated.
*/
public boolean isPreselect()
{
return (invalidComponent == null);
}
/**
* Respond to a record change event by invalidating the current preselect
* results, if currently in preselect mode and the state change impacts
* the validity of the current results.
*
* @param event
* Event which describes the DMO state change.
*
* @throws PersistenceException
* if any error occurs while processing the state change event.
*/
public void stateChanged(RecordChangeEvent event)
throws PersistenceException
{
boolean preselect = isPreselect();
boolean invalidate = true;
boolean cacheAll = false;
int jumpToRowNumber = -1;
if (adapter.isConnected())
{
storeReferenceRow(event);
setupInvalidationProperties();
if (event.isDelete())
{
// TODO: make this check more specific and more restrictive.
// Specifically, we only want to invalidate if we know the
// deleted record is included in our preselected results (not
// including the record currently being visited).
// Optimization: if the source of the event is a buffer used by
// this query, do not go into dynamic mode. Do not perform this
// optimization for scrolling queries, which can navigate back to
// the deleted record.
if (!isScrolling())
{
ArrayList<QueryComponent> components = components();
for (int i = 0; invalidate && i < components.size(); i++)
{
QueryComponent comp = components.get(i);
RecordBuffer buffer = comp.getBuffer();
if (event instanceof GlobalChangeEvent)
{
GlobalChangeEvent gce = (GlobalChangeEvent) event;
if (gce.getEntity().equals(buffer.getEntityName()))
{
Record bDMO = buffer.getCurrentRecord();
Long pk = gce.getPrimaryKey();
if (bDMO != null && !bDMO.primaryKey().equals(pk))
{
continue;
}
}
else
{
continue;
}
}
else if (buffer != event.getSource())
{
continue;
}
if (cacheOnReval)
{
invalidate = false;
}
cacheOnReval = isPreselect();
// The cacheAll optimization is meant to handle the case of
// a delete loop. If the first record we visited in a
// progressive result set is deleted by our backing buffer,
// we assume a primary purpose of the current loop is to
// delete the records found by the query. This is not
// guaranteed, but it is the common case for the
// circumstances we test for below. The idea is to let the
// invalidation proceed, but then immediately to re-find all
// of the remaining records by re-executing the query.
int rowNumber = adapter.getRowNumber();
if (invalidate &&
cacheOnReval &&
(adapter.getResults() instanceof ProgressiveResults) &&
(rowNumber == 0 ||
this.getTableCount() == 1 && revalCount == 0 && shouldExecuteForward()))
{
cacheAll = true;
jumpToRowNumber = rowNumber;
}
}
}
}
else if (event.isInsert())
{
if (preselect || breakValue != null)
{
invalidate = doubleCheckInvalidation(event);
}
if (invalidate)
{
cacheOnReval = false;
}
}
else
{
if (preselect || breakValue != null)
{
// Quick out: AdaptiveQuery sorts on an index only, which
// cannot contain extent fields. If the event is about extent
// field changes only, we do not need to invalidate.
if (event.isExtentFieldChangeOnly())
{
invalidate = false;
}
else
{
// This is the update case. If any property changed is one
// of the properties used by this query to determine the
// content or sort order of the result set, we invalidate.
// Otherwise, do nothing.
String entity = event.getEntity();
Set<String> indexProps = invalidationProperties.get(entity);
Set<String> changedProps = event.getProperties().keySet();
// Sanity check.
if (indexProps == null || changedProps == null)
{
return;
}
// Test each changed property to see if it is a member of
// the set of index properties for the modified DMO type.
invalidate = false;
Iterator<String> iter = changedProps.iterator();
while (iter.hasNext())
{
if (indexProps.contains(iter.next()))
{
invalidate = doubleCheckInvalidation(event);
break;
}
}
}
}
if (invalidate)
{
cacheOnReval = false;
}
}
if (invalidate)
{
if (preselect)
{
invalidate(event);
}
else
{
resetDynamicScrolling();
}
}
boolean debug = LOG.isLoggable(Level.FINE);
if (debug)
{
if (invalidate)
{
LOG.log(Level.FINE, "ALLOWED invalidation " + this);
}
else
{
LOG.log(Level.FINE, "AVOIDED invalidation " + this);
}
}
// The first record found by this query was deleted. Switch back
// into preselect mode immediately, and re-find all (remaining)
// matching records with a scrolling query that will not be subject
// to the "dropped" records problem encountered when using
// ProgressiveResults within a delete loop.
if (cacheAll)
{
if (debug)
{
LOG.log(Level.FINE, "Forcing full cache of results after delete event " + this);
}
// Allows execute() to determine that we now are operating in preselect mode.
invalidComponent = null;
// Allows super.execute() to determine that we need a new preselect result set.
setResults(null);
// Retrieve preselect result set. Adapter will be reconnected to
// new delegate results.
execute();
if (adapter.isConnected())
{
// restore the old state of the results position
if (jumpToRowNumber > 0 && adapter.setRowNumber(jumpToRowNumber))
{
// the query might have run off-end in the process
this.betweenRows = true;
}
}
else
{
LOG.log(Level.WARNING, "Can't restore state of query after caching.");
}
}
}
// Prevent a transition from dynamic to preselect mode.
if (invalidate && !preselect && breakValue != null)
{
breakValue = null;
if (LOG.isLoggable(Level.FINEST))
{
LOG.log(Level.FINEST, "Cancelling transition from dynamic to preselect mode");
}
}
}
/**
* This function is called when ProgressiveResults has detected changes in retrieved results.
* It switches from preselect to dynamic mode.
*/
public void resultsChanged()
{
errorCondition = true;
invalidate();
}
/**
* This function is called when ProgressiveResults has detected that the delegate results
* are closed, this invalidates any caching done on the results.
*/
public void delegateChanged()
{
resultsCached = false;
}
/**
* Explicitly close the prepared query.
*
* @param inResource
* Flag indicating this query is used in a QUERY legacy resource.
*/
@Override
public void close(boolean inResource)
{
if (inResource && components == null)
{
// nothing to close, the query was not initialized yet.
// This happens only for dynamic queries, when the query was explicitly closed or
// re-prepared before having the chance to be fully initialized with query-open
return;
}
super.close(inResource);
}
/**
* Clean up query resources when this query's scope ends. This
* implementation deregisters for global change notifications, if
* necessary, then delegates to the parent's implementation.
*
* @see PreselectQuery#cleanup()
*/
public void cleanup()
{
// if it was added in the global scope of ChangeBroker allow the listener(s) to be
// removed from the, otherwise the ChangeBroker will drop it automatically when the
// respective scope is finished
if (unregisterOnCleanup)
{
ChangeBroker broker = ChangeBroker.get();
broker.removeFromGlobal(this);
unregisterOnCleanup = false;
}
if (closed)
{
return;
}
if (components != null)
{
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
AdaptiveComponent next = (AdaptiveComponent) components.get(i);
next.cleanup();
}
}
super.cleanup();
highLevelResults = null;
}
/**
* Makes this query scrolling. This method must be invoked before any results are retrieved.
*/
public void setScrolling()
{
super.setScrolling();
cursor = new Cursor(this);
}
/**
* Set the forward-only attribute to the given value and resets the <code>cursor</code>
* if necessary.
*
* @param forwardOnly
* <code>true</code> to enable forward-only, <code>false</code> to disable it.
*/
@Override
public void setForwardOnly(boolean forwardOnly)
{
super.setForwardOnly(forwardOnly);
if (forwardOnly)
{
cursor = null;
}
}
/**
* Indicate whether this query is a preselect query by its nature.
*
* @return <code>false</code>.
*/
public boolean isNativelyPreselect()
{
return false;
}
/**
* Deletes the current row of a query's result list. Implements the 4GL
* DELETE-RESULT-LIST-ENTRY() method.
*
* @param allowBetweenRows
* If <code>true</code> then the cursor can be positioned between rows (the next
* row will be deleted). <code>false</code> for conventional DELETE-RESULT-LIST-ENTRY()
* mode where the cursor should be positioned on a row (otherwise <code>false</code> is
* returned).
*
* @return <code>true</code> on success.
*/
public logical deleteResultListEntry(boolean allowBetweenRows)
{
if (isScrolling())
{
boolean res = cursor.deleteResultListEntry(allowBetweenRows);
if (res)
{
currentRowDeleted = true;
}
return new logical(res);
}
else if (!currentRowDeleted && foundFirst)
{
currentRowDeleted = true;
return new logical(true);
}
return new logical(false);
}
/**
* Creates an entry in the result list for the current row. Implements the 4GL
* CREATE-RESULT-LIST-ENTRY() method. Created entry contains id(s) of the record(s) currently
* located in the backing buffer(s).
*
* @return <code>true</code> on success.
*/
@Override
public logical createResultListEntry()
{
cursor.addResultAtCurrentPosition(getCurrentIds(false));
return new logical(true);
}
/**
* Force this query to operate in dynamic retrieval mode, if it supports this mode.
*/
@Override
public void forceDynamicOperation()
{
avoidRevalidation = true;
invalidate();
}
/**
* Indicate whether this query type is scrolling by its nature.
*
* @return <code>false</code>.
*/
protected boolean isNativelyScrolling()
{
return false;
}
/**
* Report whether this query must check for changes across all sessions and
* possibly invalidate its current results if any are found. If not, this
* query will only honor local changes to invalidate results.
*
* @return <code>true</code> to require global invalidation checks;
* <code>false</code> to only check locally for changes which may
* invalidate results.
*/
protected boolean isGlobalInvalidation()
{
return globalInvalidation;
}
/**
* Report whether this query must operate in a one-by-one manner to honor the dirty
* changes in the same session.
*
* @return <code>true</code> to require force dynamic mode right from the start.
*
*/
protected boolean isLocalInvalidation()
{
return localInvalidation;
}
/**
* 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.
*/
@Override
void addDynamicSortCriterion(FieldReference fr, boolean asc)
{
if (fr == null || components == null)
{
return; // no-op
}
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
QueryComponent comp = components.get(i);
if (fr.getParentBuffer() == comp.getBuffer() && comp instanceof AdaptiveComponent)
{
((AdaptiveComponent) comp).addDynamicSortCriterion(fr, asc);
return;
}
}
}
/**
* Clears any dynamically sort criteria added at runtime. The query sort order will be restored
* to its form from generated at conversion time.
*/
void clearDynamicSortCriteria()
{
if (components == null)
{
return;
}
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
QueryComponent comp = components.get(i);
if (comp instanceof AdaptiveComponent)
{
((AdaptiveComponent) comp).clearDynamicSortCriteria();
}
}
}
/**
* Assemble the restriction (i.e., where) clause for an FQL query. If more
* than one component contains a where clause, they are combined with a
* logical "and". Form of the clause is:
* <pre>
* where ({component0_clause})
* [and ({component1_clause})
* [...]]
* </pre>
* <p>
*
* @param sortCriteria
* List of <code>SortCriterion</code> objects which describe the
* sort behavior for this query.
* @param indices
* int which represents the position of the component we generate
* the where clause for.
* @param whereClause
* FQLExpression which represents the variable where the "where" clause is
* going to be saved.
* @param onClause
* FQLExpression which represents the variable where the "on" clause is
* going to be saved.
* @param isOn
* boolean that will help to distinguish between "where" and "and" and "on".
*
* @throws PersistenceException
* if there is an error creating the <code>FQLPreprocessor</code>
* from which the preprocessed where clause is retrieved for each
* query component.
*
*/
@Override
protected void assembleWhereClause(ArrayList<SortCriterion> sortCriteria,
int indices,
FQLExpression whereClause,
FQLExpression onClause,
boolean isOn)
throws PersistenceException
{
super.assembleWhereClause(sortCriteria, indices, whereClause, onClause, isOn);
// If this query was last in dynamic mode, we need to use the break
// value provided by the DynamicResults object as a reference point at
// which to begin our results. Put the comparison operation in the
// where clause with a placeholder which will be substituted with the
// break value at execute time.
if (breakValue != null)
{
if (whereClause.isEmpty())
{
whereClause.append(" where (");
}
else
{
whereClause.append(" and (");
}
SortCriterion crit = sortCriteria.get(0);
if (TemporaryBuffer.MULTIPLEX_FIELD_NAME.equals(crit.getUnqualifiedName()))
{
// skip the _multiplex component as this appears first for temp-tables
crit = sortCriteria.get(1);
}
whereClause.append(crit.toWhereExpression(true, false));
whereClause.append(") ");
}
}
/**
* Assemble the {@code order by} clause for an FQL query by combining the sort criteria from each query
* component sequentially.
* Any text-based sort criteria are augmented with the necessary {@code upper()} and {@code rtrim()}
* functions.
*
* @return List of {@code SortCriterion} objects which describe the sort behavior for this query.
*
* @throws PersistenceException
* if the sort clause of any query component contains any unqualified property names;
* if a sort clause contains a reference to an unrecognized record buffer;
* if there is an error creating the {@code HQLPreprocessor} from which setup data is required.
*/
@Override
protected ArrayList<SortCriterion> assembleOrderByClause()
{
ArrayList<SortCriterion> all = new ArrayList<>();
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
AdaptiveComponent next = (AdaptiveComponent) components.get(i);
switch (next.getIteration())
{
case FIRST:
case LAST:
if (i > 0)
{
// sort clause for FIRST/LAST is only used for the outermost component
continue;
}
break;
default:
break;
}
List<SortCriterion> sortCriteria = next.getSortCriteria();
sortCriteria = assembleOrderByClause(sortCriteria);
all.addAll(sortCriteria);
}
return all;
}
/**
* Prepare query substitution parameters for this query. In addition to the basic parameters
* handled by the superclass, this method will add the query's {@code breakValue} as a
* parameter if it is non-null. A non-{@code null} {@code breakValue} indicates that the query
* is shifting from dynamic mode to preselect mode. All records retrieved from this point
* forward must not include those already retrieved, and the {@code breakValue} demarcates
* this boundary. The where clause will have been modified to include a comparison operation
* with the {@code breakValue} in this case.
*
* @param args
* Argument list to be populated.
*/
@Override
protected void prepareParameters(List<Object> args)
throws PersistenceException
{
super.prepareParameters(args);
if (breakValue == null && invalidComponent != null)
{
RecordBuffer buffer = invalidComponent.getBuffer();
Record dmo = buffer.getSnapshot();
if (dmo != null)
{
AdaptiveComponent comp = (AdaptiveComponent) components().get(0);
List<SortCriterion> sortCriteria = comp.getSortCriteria();
int len = sortCriteria.size();
Method getter = null;
for (int i = 0; i < len && getter == null; i++)
{
SortCriterion crit = sortCriteria.get(i);
getter = crit.getMethod();
}
if (getter != null)
{
try
{
PropertyMeta meta = buffer.getDmoInfo().getPropsByGetterMap().get(getter);
breakValue = meta.getDataHandler().getField(dmo, meta.getOffset());
}
catch (Exception exc)
{
DBUtils.handleException(buffer.getDatabase(), exc);
LOG.log(Level.SEVERE, exc.getMessage(), exc);
}
}
}
}
if (breakValue != null)
{
args.add(breakValue);
}
}
/**
* Overrides the parent's pure preselect implementation to use a different
* retrieval techniques depending upon the current mode of the query. As
* long as the query remains in preselect mode, the superclass' approach
* is used. If, however, the preselect result set has been invalidated by
* a DMO state change, this method creates engages the "backup" dynamic
* retrieval mechanism.
* <p>
* In this case, a logical <code>DynamicResults</code> object is created
* the first time through this method after invalidation. Thereafter,
* this object is simply used as a regular {@link Results} object, in that
* parent's {@link PreselectQuery#fetch} method uses the new results
* object without knowing or caring that it has been substituted with a
* dynamic implementation.
* <p>
* In addition, if this query is set to check for global DMO change events,
* such events are collected and processed, which will possibly invalidate
* the query's current results, or cause a change to dynamic mode.
*
* @return <code>true</code> if the underlying preselect result set was
* created during this invocation; <code>false</code> if query
* was executed previously and we already have a preselect result
* set.
*
* @throws ErrorConditionException
* if an error occurs executing the preselect query.
* @throws IllegalStateException
* if no query component has been added to this query.
*/
protected boolean execute()
{
// Check if all components with a non-null JOIN have a valid inverse
// record on which to perform the join
if (!verifyJoins())
{
return true;
}
// Possibly invalidate if another session has made a change.
if (DirtyShareSupport.isEnabledCrossSession() &&
DirtyShareSupport.isGlobalNotificationEnabled() &&
isGlobalInvalidation())
{
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
comp.processGlobalEvents();
}
}
if (isPreselect())
{
try
{
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
comp.initialize();
}
return super.execute();
}
catch (PersistenceException exc)
{
ErrorManager.handleStopException(new StopConditionException(exc));
}
}
if (adapter.isConnected())
{
return false;
}
// If we call super.execute() above, a cleaner is registered for us,
// but if not, we have to do it here.
registerCleaner();
try
{
// Create a dynamic results object which will drive record retrieval.
DynamicQuery query = null;
int tc = getTableCount();
if (tc > 1)
{
query = createCompoundQuery(invalidComponent);
}
else
{
query = createSimpleQuery(invalidComponent);
}
boolean[] idOnly = new boolean[tc];
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
QueryComponent comp = components.get(i);
idOnly[i] = !isClientWhere() && !isFullRecords() && comp.isIdOnly();
}
DynamicResults dr = new DynamicResults(query, idOnly);
Results hlr = createResults(dr);
setResults(hlr);
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
return false;
}
/**
* Respond to the condition that a component with a non-null JOIN has a
* missing inverse on which to perform the join.
* <p>
* This implementation is a placeholder which currently defers to the
* parent's implementation.
*/
protected void handleMissingJoinRecord()
{
/*
if (!isPreselect() || revalCount > 0)
{
throw new QueryOffEndException("Missing join record");
}
*/
super.handleMissingJoinRecord();
}
/**
* Indicate whether a call to move to the previous record in the result list should be treated
* as a call to move to the last record in the list. This will be the cases listed in the super
* function {@link PreselectQuery#treatPreviousAsLast}, but only if the query is in preselect
* mode. That fixes previous mistakenly treated as last after query invalidation.
*
* @return <code>true</code> to treat a previous request as a request to move to the last
* record in the list; else <code>false</code>.
*/
protected boolean treatPreviousAsLast()
{
return isPreselect() && super.treatPreviousAsLast();
}
/**
* Execute the query to create a first level result set.
* <p>
* This case is for dialects that do not support lazy queries:
* If we are executing the query after a re-validation, or if the query's sort phrase does not match
* any legacy index, fall back to the superclass' implementation. Otherwise, this implementation creates
* an instance of {@code ProgressiveResults}, which defers actually executing the query until
* the first request to fetch a result.
*
* This case is for dialects that support lazy queries:
* This implementation can create either a {@code ScrollingResults} or a type of {@code Results}
* determined by the execution of the super function.
*
* @param persistence
* Object which is used to execute the query.
* @param fql
* FQL query string.
* @param args
* Substitution parameters to be inserted into placeholders within the FQL query
* string.
*
* @return An instance of {@code ProgressiveResults} which fetches progressively larger
* brackets of query results as the results are accessed (for dialects that do not support lazy
* queries),
* Or
* An instance of {@code ScrollingResults}, possibly fetched in a lazy manner (for dialects that
* do support lazy queries).
*
* @throws PersistenceException
* not thrown by this implementation.
*/
@Override
protected Results executeQuery(Persistence persistence, String fql, Object[] args)
throws PersistenceException
{
Results results = null;
if (isLazyMode())
{
Long templateRowid = getTemplateQueryRowid(args);
if (templateRowid != null)
{
results = super.executeQuery(persistence, fql, args);
return results;
}
return new ScrollingResults(persistence, executeScroll(persistence, fql, args, true));
}
if (forceFullResults || cacheOnReval || probablyRequiresResort())
{
results = super.executeQuery(persistence, fql, args);
}
else
{
// determine whether this is a (primary key only) projection query
boolean projection = !isClientWhere() && !isFullRecords();
ArrayList<QueryComponent> components = components();
for (int i = 0; projection && i < components.size(); i++)
{
QueryComponent comp = components.get(i);
projection = comp.isIdOnly();
}
// create the progressive results
ProgressiveResults pr = new ProgressiveResults(persistence,
fql,
args,
isScrolling(),
isForwardOnly(),
getEntities(),
projection,
getDataRelation());
pr.addResultsChangeListener(this);
results = pr;
}
return results;
}
/**
* Function responsible with returning a list of {@code ScrollableResults}.
* It converts the FQL into SQL and executes the query against the SQL server.
* If the query is intended for temporary tables, then the execution could be done in a lazy manner.
*
* @param <T>
* The type of the rows returned in the list.
* @param persistence
* Object which is used to execute the query.
* @param fql
* FQL query string.
* @param args
* Substitution parameters to be inserted into placeholders within the FQL query
* string.
* @param lazyMode
* Flag that indicates whether the query should be executed in a lazy manner.
* Not all queries can be executed with this option, but the checks for that are not done here.
*
* @return A scrollable list of rows of type {@code <T>} from SQL server, as specified by the
* {@code fql} query.
*
* @throws PersistenceException
* when an error occurred while performing the requested operations.
*/
protected <T> ScrollableResults<T> executeScroll(Persistence persistence,
String fql,
Object[] args,
boolean lazyMode)
throws PersistenceException
{
int scrollMode = isScrolling() || lazyMode ? ResultSet.TYPE_SCROLL_INSENSITIVE
: ResultSet.TYPE_FORWARD_ONLY;
return persistence.scroll(getEntities(), fql, args, 0, 0, getDataRelation(), scrollMode, lazyMode);
}
/**
* Override the parent's implementation to intercept result creation, and
* always return our adapter, after setting the <code>results</code>
* parameter as the adapter's delegate.
*
* @param results
* An object which manipulates the database result set returned
* from execution of the query.
*
* @return <code>Results</code> object.
*
* @throws PersistenceException
* if there is any error creating results.
*/
@Override
protected Results createResults(Results results)
throws PersistenceException
{
adapter.setResults(results);
betweenRows = false;
if (highLevelResults == null)
{
highLevelResults = super.createResults(adapter);
}
return highLevelResults;
}
/**
* Create a single-table, dynamic query to be used as the worker object
* for a <code>DynamicResults</code> object when this query switches from
* preselect to dynamic mode. Creates a modified implementation of
* <code>RandomAccessQuery</code>, which does not reset the record buffer
* upon instantiation.
*
* @param invalidComp
* Query component which caused this query to be invalidated.
*
* @return A <code>RandomAccessQuery</code> based upon this query's single
* component.
*
* @throws PersistenceException
* if there is an error creating the <code>HQLPreprocessor</code>
* from which setup data is required.
*/
protected DynamicQuery createSimpleQuery(AdaptiveComponent invalidComp)
throws PersistenceException
{
invalidComp.translateWhere();
// Always do the initial fetch with lock type of NONE. It will be
// overridden as necessary in PreselectQuery.fetch().
LockType lockType = LockType.NONE;
AdaptiveComponent comp = (AdaptiveComponent) components().get(0);
comp.translateWhere();
Object[] args = comp.getRawArgs();
if (parameterFilter != null)
{
args = parameterFilter.filterParameters(args);
}
RandomAccessQuery query = new RandomAccessQuery();
query.initialize(comp.getBuffer().getDMOProxy(),
comp.getOriginalWhere(),
null,
comp.getOriginalSort(),
comp.getInverse(),
args,
lockType);
query.setForwardOnly(isForwardOnly());
query.overrideIncludeFields(included);
query.overrideExcludeFields(excluded);
if (this.foundFirst)
{
query.setRecordAlreadyFound(true);
}
RecordBuffer[] ext = getExternalBuffers();
if (ext != null)
{
query.addExternalBuffers(ext);
}
setWorkerRefererenceRecord(query);
return query;
}
/**
* Create a multi-table, compound query to be used as the worker object
* for a <code>DynamicResults</code> object when this query switches from
* preselect to dynamic mode. Uses the parent query's components to
* define a <code>Joinable</code> instance for each of the compound
* query's own components.
* <p>
* Single-table <code>AdaptiveQuery</code> instances are used for every
* joinable component, except the one corresponding to the buffer which
* caused invalidation of the preselect result set. For the component
* corresponding with this buffer, an instance of
* <code>RandomAccessQuery</code> is used instead.
*
* @param invalidComp
* Query component which caused this query to be invalidated.
*
* @return A <code>CompoundQuery</code> based upon this query's multiple
* components.
*
* @throws PersistenceException
* if there is an error creating the <code>HQLPreprocessor</code>
* from which setup data is required.
*/
protected DynamicQuery createCompoundQuery(AdaptiveComponent invalidComp)
throws PersistenceException
{
invalidComp.translateWhere();
CompoundQuery compoundQuery = new CompoundQuery()
{
/**
* Don't allow this compound query, which was de-optimized, to be re-optimized, in
* order to prevent thrashing back and forth between the two modes.
*/
@Override
protected boolean canOptimize()
{
return false;
}
};
compoundQuery.initialize(false, false);
// iterate the adaptive components and add their fallback compound query components into
// the new query, forcing each into dynamic mode
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
comp.translateWhere();
CompoundComponent fallback = comp.getFallback();
fallback.getQuery().forceDynamicOperation();
compoundQuery.addCompoundComponent(fallback);
}
return compoundQuery;
}
/**
* Reset the state of the results adapter to its initial default state and
* invoke the superclass' overridden method.
*/
protected void resetResults()
{
resultsCached = false;
super.resetResults();
if (adapter != null)
{
adapter.cleanup();
adapter.setResults(null);
}
}
/**
* Get array of the primary keys of the records contained in the backing buffers.
*
* @return array of the primary keys of the records contained in the backing buffers or
* <code>null</code> if any of the buffers doesn't contain a record.
*/
protected Object[] getCurrentIds()
{
return getCurrentIds(true);
}
/**
* Get array of the primary keys of the records contained in the backing buffers.
*
* @param nullable
* If a buffer doesn't contain a record, then depending on the value of this
* parameter, <code>null</code> is returned (if <code>nullable</code> is
* <code>true</code>) or <code>null</code> element is added to the resulting array (if
* <code>nullable</code> is <code>false</code>).
*
* @return array of the primary keys of the records contained in the backing buffers or
* <code>null</code> if any of the buffers doesn't contain a record and
* <code>nullable</code> is <code>true</code>.
*/
protected Object[] getCurrentIds(boolean nullable)
{
List<Long> data = new ArrayList<>();
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
QueryComponent comp = components.get(i);
RecordBuffer buffer = comp.getBuffer();
if (!buffer.isAvailable())
{
if (nullable && !comp.isOuter())
{
return null;
}
else
{
data.add(null);
}
}
else
{
data.add(buffer._rowID());
}
}
return data.toArray();
}
/**
* Load all records for a given result row into their backing buffers.
*
* @param data
* An array of primary key IDs or DMOs representing records to be fetched for the
* composite row.
* @param lockType
* Overriding lock type to apply to fetched records. If null, default lock types are
* applied.
*
* @return <code>true</code> if the record(s) were successfully loaded.
*/
protected boolean loadByValues(Object[] data, LockType lockType)
{
currentRowDeleted = false;
try
{
if (data == null)
{
coreFetch(null, lockType, false, false, true);
return false;
}
offEnd = OffEnd.NONE; // required for PreselectQuery to fetch properly
boolean fetched = coreFetch(data, lockType, true, false, true);
accumulate();
return fetched;
}
catch (MissingRecordException e)
{
return false;
}
catch (PersistenceException e)
{
ErrorManager.recordOrThrowError(e);
return false;
}
}
/**
* Fetch all records for a given result row into their backing buffers.
* Each result row contains one column per query component. Each column
* contains the unique ID of the record to be fetched or the record itself.
* If the latter, the record is refetched in case it has changed since the
* record was first retrieved. If the scrollable cursor backing this query
* currently is at a position where no retrieval is possible, the backing
* buffers will have their current records released.
*
* @param available
* <code>true</code> if the most recent scrollable cursor move
* left the cursor at an available result row; <code>false</code>
* if the cursor cannot read a result at its current position.
* @param lockType
* Overriding lock type to apply to fetched records. If
* <code>null</code>, default lock types are applied.
* @param errorIfNull
* <code>true</code> if failed retrievals should raise error.
*
* @throws MissingRecordException
* if the record was expected to be available, but wasn't. This
* typically indicates the record was deleted between the time it
* initially was found by the query and the time it was to be
* fetched to store in the record buffer.
* @throws ErrorConditionException
* if the fetch of any record fails or if no records are available
* and the query is set to fail in this case.
* @throws QueryOffEndException
* if no records are available and the query is set not to fail
* in this case.
*/
protected void fetch(boolean available, LockType lockType, boolean errorIfNull)
throws MissingRecordException
{
super.fetch(available, lockType, errorIfNull);
if (!isScrolling() && !foundFirst && available && offEnd == OffEnd.NONE)
{
foundFirst = true;
}
}
/**
* Store the last fetched row as the reference row for future iterations.
*
* @param event
* Event caused by update of a record of specific type.
*/
protected void storeReferenceRow(RecordChangeEvent event)
{
if (cursor != null)
{
cursor.storeReferenceRow(event);
}
}
/**
* Invalidate the query if reference row exists.
*/
protected void invalidateIfReferenceRowExists()
{
if (cursor != null)
{
Record[] row = cursor.getReferenceRow();
if (row != null)
{
invalidate();
}
}
}
/**
* For single-table query: set the first component of the reference row as the reference record
* for the worker query.
*
* @param query
* Worker query.
*/
protected void setWorkerRefererenceRecord(RandomAccessQuery query)
{
if (cursor != null)
{
Record[] persistable = cursor.getReferenceRow();
if (persistable != null)
{
query.setReferenceRecord(persistable[0]);
}
}
}
/**
* Check if this query should operate in dynamic mode right from the start.
*
* @return {@code true} if this was invalidated early (before even being executed).
*/
protected boolean earlyInvalidate()
{
if (DirtyShareSupport.isEnabledIntraSession() && isLocalInvalidation())
{
// invalidate if this session has some dirty changes on a component.
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
if (comp.isDirty(true))
{
forceDynamicOperation();
comp.getBuffer().allowReversibleOffEnd();
comp.getBuffer().resetSortIndex();
return true;
}
}
}
return false;
}
/**
* Invalidate the preselected result set of this query, which effectively
* switches this query from preselect mode to dynamic mode.
*
* @param invalidComponent
* Query component which is triggering invalidation.
*/
void invalidate(AdaptiveComponent invalidComponent)
{
if (!isPreselect())
{
// If we are already in dynamic mode, invalidation means clearing the
// dynamic query's cursor cache. This prevents us from getting a
// potentially stale value for first() and last() navigation.
resetDynamicScrolling();
}
if (isPreselect() && quickRevalidate(invalidComponent))
{
this.invalidComponent = null;
}
else
{
this.invalidComponent = invalidComponent;
}
if (adapter.isConnected())
{
resetResults();
}
else
{
clearRepoCache();
}
adapter.prepareDelegateChange();
}
/**
* Check if we can quickly invalidate and revalidate back. The original intent is to invalidate, but this
* routine can signal if the revalidation can be done straight after. This happens when there is no actual
* reference row and the query would operate in dynamic mode from the very start. In this case, prefer to
* revalidate on spot.
* <p>
* The usual case for this quick revalidate is a dynamic query that was just opened. Its behavior is to
* execute the query, even if there was not actual NEXT/PREV/etc. Thus, there are many queries that are
* opened and not yet used. Such case leads to useless invalidations, that may even get stuck in dynamic
* mode.
*
* @param invalidComponent
* Query component which is triggering invalidation.
*
* @return {@code true} if a quick revalidation can happen. This means dismissing the invalidation.
* {@code false} if the invalidation is required to happen. Note that even if this returns true,
* the results should be reset and a new delegate requested. Returning true doesn't mean that
* the current results are good!
*/
private boolean quickRevalidate(AdaptiveComponent invalidComponent)
{
if (this.avoidRevalidation ||
(this.invalidComponent != null && this.invalidComponent != invalidComponent) ||
invalidComponent == null)
{
return false;
}
if (invalidComponent.getBuffer().getSnapshot() == null && // there is no buffer snapshot
(this.cursor == null || this.cursor.getReferenceRow() == null)) // and there is no reference row
{
// there is no actual reference row from which the RAQ can continue, so the invalidated query
// won't ever find a suitable break value: attempt a quick revalidation now
if (++revalCount > MAX_REVALIDATIONS)
{
if (revalCount == 4 && LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "AdaptiveQuery permanently forced to dynamic mode: " + this);
}
return false;
}
// if the execution reaches this point, it means that we did a quick
// invalidate -> revalidate
return true;
}
else
{
return false;
}
}
/**
* Helper method to understand if this query is going to use lazy query results mode.
* Such mode automatically solves invalidation at the database side, so there is no
* need to invalidate in the server-side.
*
* @return {@code true} if this is a lazy mode query with implicit invalidation handling
*/
private boolean isLazyMode()
{
if (lazyMode == null)
{
ArrayList<QueryComponent> components = components();
if (components == null || components.size() != 1)
{
return false;
}
RecordBuffer buffer = components.get(0).getBuffer();
lazyMode = buffer != null &&
buffer.isActive() &&
buffer.getPersistence().supportsLazyQueryResultsMode();
}
return lazyMode;
}
/**
* Indicate whether the query results as initially fetched are likely to require a re-sort before they
* are returned. This decision is based on whether the sort phrase exists and does not match any legacy
* index of the first query component table.
* <p>
* This method makes an assumption (which will not be correct in all cases) that if an index on the first
* table exactly matches the sort phrase, it will be used to fetch the results. However, the database
* query planner may well select a different index, based on the WHERE clause. In that case, this method
* will report incorrectly that a re-sort is not required, which may lead to "chunking" the
* result set with {@code ProgressiveResults} when this is not the most efficient approach. However, it
* may help detect some cases where a re-sort will take place, avoiding this anti-pattern.
*
* @return {@code true} if there is no sort phrase, or if the sort phrase does not match any legacy
* index of the first query component table.
*/
private boolean probablyRequiresResort()
{
String sort = getSortPhrase();
if (sort == null)
{
// no specific sorting is required, so the result set can be returned in its initial order
// TODO: is this a valid state, or do we always have a sort phrase?
return false;
}
boolean match = false;
if (!components.isEmpty())
{
try
{
QueryComponent comp = components.get(0);
RecordBuffer buffer = comp.getBuffer();
IndexHelper ih = buffer.getIndexHelper();
if (ih.getIndexForSort(buffer, sort) != null)
{
match = true;
}
}
catch (PersistenceException exc)
{
match = false;
}
}
return !match;
}
/**
* Given a record change event for which this query is listening, determine
* whether invalidation may be avoided. For an update, assume it already
* has been determined that a relevant property (one which can affect the
* changed DMO's inclusion in preselected results) has been modified.
* <p>
* This additional check is about detecting whether it is possible that a
* modified or newly inserted record can possibly appear later in the
* preselected result set than the record we currently are visiting.
*
* @param event
* Event which describes the DMO state change.
*
* @return <code>true</code> if invalidation is confirmed;
* <code>false</code> to abandon invalidation.
*/
private boolean doubleCheckInvalidation(RecordChangeEvent event)
{
if (isScrolling())
{
return true;
}
// TODO: make this check more specific and more restrictive.
// Specifically, we only want to invalidate if we know the inserted or
// updated record would have been included in our preselected results.
boolean invalidate = true;
boolean isInsert = event.isInsert();
RecordBuffer sourceBuf = event.getSource();
Record targetDMO = event.getDMO();
ArrayList<QueryComponent> components = components();
for (int i = 0; invalidate && i < components.size(); i++)
{
AdaptiveComponent c = (AdaptiveComponent) components.get(i);
RecordBuffer buffer = c.getBuffer();
if (isInsert)
{
if (buffer != sourceBuf)
{
continue;
}
}
else
{
Record current = buffer.getCurrentRecord();
if (current != targetDMO)
{
continue;
}
}
Record snapshot = buffer.getSnapshot();
DMOSorter sorter = c.getDMOSorter();
if (sorter == null)
{
// Assume we are using default index (by primary key). In this
// case, an insert causes invalidation, an update does not.
invalidate = isInsert;
}
else
{
// Use sorter to determine whether changed record *could* appear
// further on in the results. If not, do not invalidate now.
int comp = sorter.compare(targetDMO, snapshot);
if (comp <= 0)
{
invalidate = false;
}
}
}
return invalidate;
}
/**
* If in dynamic mode, reset the cursor underlying the dynamic query in
* order to prevent stale data.
*/
private void resetDynamicScrolling()
{
if (adapter.isConnected())
{
Results res = adapter.getResults();
if (res instanceof DynamicResults)
{
((DynamicResults) res).query.resetScrolling();
}
}
}
/**
* Set up the map of DMO implementation classes to sets of properties
* which, if changed, would invalidate the current, preselected result set
* for this query. These will be properties which are used either to
* restrict (i.e., where clause) or sort (i.e., order by clause) the
* contents of the result set.
*
* @throws PersistenceException
* if there is an error creating the <code>HQLPreprocessor</code>
* from which setup data is required.
*/
private void setupInvalidationProperties()
throws PersistenceException
{
if (invalidationProperties != null)
{
return;
}
invalidationProperties = new HashMap<>();
ArrayList<QueryComponent> components = components();
for (int i = 0; i < components.size(); i++)
{
AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
// Collect property names from where clause.
Map<String, Set<String>> whereProps = comp.getRestrictionProperties();
if (whereProps != null)
{
Iterator<Map.Entry<String, Set<String>>> iter2 = whereProps.entrySet().iterator();
while (iter2.hasNext())
{
Map.Entry<String, Set<String>> next = iter2.next();
String entity = next.getKey();
Set<String> props = next.getValue();
Set<String> indexProps = invalidationProperties.get(entity);
if (indexProps == null)
{
indexProps = new HashSet<>();
invalidationProperties.put(entity, indexProps);
}
indexProps.addAll(props);
}
}
// Collect property names from sort criteria.
List<SortCriterion> sortCriteria = comp.getSortCriteria();
if (sortCriteria == null || sortCriteria.isEmpty())
{
continue;
}
Iterator<SortCriterion> iter3 = sortCriteria.iterator();
while (iter3.hasNext())
{
SortCriterion next = iter3.next();
Class<? extends Record> dmoClass = next.getDMOClass();
String entity = dmoClass.getName();
Set<String> indexProps = invalidationProperties.get(entity);
if (indexProps == null)
{
indexProps = new HashSet<>();
invalidationProperties.put(entity, indexProps);
}
indexProps.add(next.getUnqualifiedName());
}
}
}
/**
* Invalidate the preselected result set of this query, which effectively
* switches this query from preselect mode to dynamic mode.
*
* @param event
* Object which holds information about the DMO state change
* which occurred.
*/
private void invalidate(RecordChangeEvent event)
{
String entity = event.getEntity();
AdaptiveComponent comp = null;
ListIterator<QueryComponent> iter = components.listIterator(components.size());
while (iter.hasPrevious() && comp == null)
{
AdaptiveComponent next = (AdaptiveComponent) iter.previous();
if (next.getBuffer().getEntityName().equals(entity))
{
comp = next;
}
}
assert comp != null;
invalidate(comp);
if (LOG.isLoggable(Level.FINEST))
{
Exception exc = new Exception("Details (not an error): [" + getHQL() + "]");
LOG.log(Level.FINEST,
"Adaptive query's result set invalidated by: " +
invalidComponent.getBuffer().toString(event.getDMO()) +
(event.isDelete()
? "Record deleted"
: "Affected properties: " + event.getProperties()),
exc);
}
}
/**
* Invalidate the query by using the last query component as the invalid component. For a
* multi-table query, this will force all components above this to start in dynamic mode.
*/
private void invalidate()
{
ListIterator<QueryComponent> iter = components.listIterator(getTableCount());
if (iter.hasPrevious())
{
AdaptiveComponent comp = (AdaptiveComponent) iter.previous();
invalidate(comp);
}
}
/**
* Begin the transition of this query from dynamic to preselect mode. The
* remaining work of this transition is performed in the {@link #fetch}
* and {@link #execute} methods.
*
* @param breakValue
* The sort band break value which indicates that the current,
* dynamic query has crossed a sort band boundary.
*/
private void revalidate(Object breakValue)
{
if (avoidRevalidation)
{
return;
}
// Prevent thrashing between modes too often. If we are attempting to
// revalidate more than 3 times, we probably are in a use case which is
// better off staying in dynamic mode.
// TODO: make this heuristic more intelligent, possibly by taking into
// account the frequency of mode changes and the overall number of
// records traversed.
if (++revalCount > MAX_REVALIDATIONS)
{
if (revalCount == 4 && LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "AdaptiveQuery permanently forced to dynamic mode: " + this);
}
return;
}
this.breakValue = breakValue;
adapter.prepareDelegateChange();
if (LOG.isLoggable(Level.FINEST))
{
Exception exc = new Exception("Details (not an error): [" + getHQL() + "]");
LOG.log(Level.FINEST,
"Adaptive query reset to preselect mode at record: " +
invalidComponent.getBuffer().getDMOInterface().getName() +
"#" +
adapter.getID(0) +
"; breakValue = " +
breakValue,
exc);
}
}
/**
* Mark the query to retrieve the entire result set (useful for FILL operations).
*/
public void hintFullResults()
{
if (isScrolling())
{
// cannot move to first if we are not scrolling
this.forceFullResults = true;
}
}
/**
* An implementation of the {@link Results} interface which uses a single
* table or multi-table <code>DynamicQuery</code> as a delegate. An
* instance of this method replaces the enclosing query's preselected
* results when a DMO state change invalidates those results. If a single
* table must be accessed, a modified <code>RandomAccessQuery</code> is
* used as a delegate. For multiple tables, a <code>CompoundQuery</code>
* is used.
* <p>
* The adaptive query's original query components are interrogated for
* their properties in order to create the dynamic query (and its
* components in the multi-table case).
*/
private class DynamicResults
implements Results
{
/** Underlying query which manages the navigation of results */
private final DynamicQuery query;
/** All buffers being updated by the dynamic query */
private final RecordBuffer[] buffers = getRecordBuffers();
/** ID-only flags, ordered by column */
private final boolean[] idOnly;
/** Flag indicating this results object has been closed */
private boolean closed = false;
/**
* Constructor which accepts a backing, dynamic query and turns
* scrolling on for that query.
*
* @param query
* Dynamic query which will back this results object.
* @param idOnly
* Array of ID only flags, ordered by column.
*
* @throws PersistenceException
* if there is an error beginning a new transaction.
*/
DynamicResults(DynamicQuery query, boolean[] idOnly)
throws PersistenceException
{
this.query = query;
this.idOnly = idOnly;
query.setScrolling();
query.setLenientOffEnd(true);
}
/**
* Move cursor to the first results row.
*
* @return <code>true</code> if there are any results.
*/
public boolean first()
{
query.first();
return postFetch(false);
}
/**
* Move cursor to the last results row.
*
* @return <code>true</code> if there are any results.
*/
public boolean last()
{
query.last();
return postFetch(true);
}
/**
* Move cursor to the next results row.
*
* @return <code>true</code> if there is a result under the cursor after the move.
*/
public boolean next()
{
query.next();
return postFetch(true);
}
/**
* Move cursor to the previous results row.
*
* @return <code>true</code> if there is a result under the cursor after the move.
*/
public boolean previous()
{
query.previous();
return postFetch(false);
}
/**
* Is the cursor on the first row in the results set?
*
* @return <code>true</code> if the cursor is on the first row.
*/
public boolean isFirst()
{
return (getRowNumber() == 0);
}
/**
* Is the cursor on the last row in the results set?
*
* @return <code>true</code> if the cursor is on the last row.
*/
public boolean isLast()
{
throw new UnsupportedOperationException();
}
/**
* Is the result delegate responsible for fetching? This is usually true when using
* query delegates which handle the fetching process already.
*
* @return {@code true} for complex results which also do the fetching
*/
@Override
public boolean isAutoFetch()
{
return true;
}
/**
* Returns the row at the current position. It can be either only an array of PK or
* an array of DMOs.
*
* @param forceOnlyId
* Indicates whether it's only the primary keys that must be returned,
* or original data found is returned (either PK or DMO).
*
* @return Object array or {@code null}.
*/
public Object[] get(boolean forceOnlyId)
{
int len = buffers.length;
Object[] row = new Object[len];
for (int i = 0; i < len; i++)
{
row[i] = get(i);
if (forceOnlyId && (row[i] instanceof Record))
{
row[i] = ((Record) row[i]).primaryKey();
}
}
return row;
}
/**
* Get the object at the current result row, at the specified column.
*
* @param column
* Zero-based index column of the desired object.
*
* @return Object at <code>column</code> or <code>null</code>.
*/
public Object get(int column)
{
Record dmo = buffers[column].getCurrentRecord();
return ((dmo != null && idOnly[column]) ? dmo.primaryKey() : dmo);
}
/**
* Get the primary key ID at the current result row, at the specified column.
*
* @param column
* Zero-based index column of the desired ID.
*
* @return ID of the record or <code>null</code>.
*/
public Long getID(int column)
{
Object datum = get(column);
return (datum != null && idOnly[column]) ? (Long) datum : ((Record) datum).primaryKey();
}
/**
* Get the row number currently under the cursor.
*
* @return Zero-based index of the current row, or <code>0</code> by
* default if the cursor's position is not currently known.
*/
public int getRowNumber()
{
integer rowNum = query.currentRowImpl();
return (rowNum.isUnknown() ? 0 : rowNum.intValue());
}
/**
* Set the row number currently under the cursor.
*
* @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)
{
reposition(row);
return postFetch(false);
}
/**
* Scroll the cursor ahead by the specified number of rows. If the
* number is negative, the cursor is moved backward. This may put the
* cursor off the end (either end) of the query's results.
*
* @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)
{
// Set the delegate query's fetchOnReposition strategy to match the
// enclosing AdaptiveQuery's current strategy.
query.setFetchOnReposition(isFetchOnReposition());
if (rows < 0)
{
query.backward(-rows);
}
else
{
query.forward(rows);
}
return postFetch(rows > 0);
}
/**
* Reset the cursor to its natural starting position, before the first result row.
*/
public void reset()
{
reposition(0);
}
/**
* Invoked when the current Hibernate session is about to close. Gives
* the implementation an opportunity to perform appropriate processing in
* response to this event.
*/
public void sessionClosing()
{
}
/**
* Clean up and release any resources which this object is holding, such
* as open result sets.
*/
public void cleanup()
{
if (!closed)
{
closed = true;
}
}
/**
* Reposition the backing query to the given row. This method sets the
* fetch-on-reposition strategy of the backing query to that of the
* enclosing query before attempting the reposition.
*
* @param row
* Zero-based index of the target row.
*/
private void reposition(int row)
{
// Set the delegate query's fetchOnReposition strategy to match the
// enclosing AdaptiveQuery's current strategy.
query.setFetchOnReposition(isFetchOnReposition());
query.reposition(row + 1); // reposition() is 1-based
}
/**
* Optionally switch the enclosing back to preselect mode if possible
* and provide a return value which indicates whether the results object
* currently is positioned on a row.
*
* @param revalidate
* <code>true</code> if query should attempt to switch back to
* preselect mode, else <code>false</code>. The switch should
* only be made if the last operation moved forward through the
* query's results.
*
* @return <code>true</code> if positioned on a row, else
* <code>false</code> if off-end.
*/
private boolean postFetch(boolean revalidate)
{
if (revalidate)
{
Object breakValue = query.getBreakValue();
if (breakValue != null)
{
revalidate(breakValue);
}
}
return !query._isOffEnd();
}
}
/**
* A {@link ResultsProvider} implementation which is invoked when
* transitioning between preselect and dynamic modes (both directions).
* Provides a callback for {@link ResultsAdapter} when a new delegate
* {@link Results delegate worker} is needed by the adapter.
*/
private class ResultsSource
implements ResultsProvider
{
/**
* Invoked by a {@link ResultsAdapter} when a new <code>Results</code>
* delegate worker is needed. Determines whether we need a preselect
* or dynamic results delegate and installs the appropriate delegate in
* the results adapter.
*
* @param adapter
* <code>ResultsAdapter</code> which is the source of the
* request.
*/
public void provideNewResults(ResultsAdapter adapter)
{
// Dynamic --> Preselect transition.
if (breakValue != null)
{
// Allows execute() to determine that we now are operating in preselect mode.
invalidComponent = null;
// Allows super.execute() to determine that we need a new
// preselect result set.
adapter.cleanup();
setResults(null);
// Retrieve preselect result set. Adapter will be reconnected to
// new delegate results.
execute();
// Store the last record(s) retrieved in dynamic mode.
int i = 0;
boolean recordsPresent = true;
Long[] ids = new Long[getTableCount()];
if (cursor != null)
{
Object[] row = cursor.peekLastLoadedResult();
if (row != null)
{
for (int j = 0; j < row.length; j++)
{
Long id = row[j] instanceof Record
? ((Record) row[j]).primaryKey()
: (Long) row[j];
ids[j] = id;
}
}
else
{
recordsPresent = false;
}
}
else
{
ArrayList<QueryComponent> components = components();
for (int j = 0; j < components.size(); j++)
{
QueryComponent comp = components.get(j);
RecordBuffer buf = comp.getBuffer();
if (buf.getCurrentRecord() != null)
{
ids[i++] = buf.getCurrentRecord().primaryKey();
}
else
{
recordsPresent = false;
break;
}
}
}
// Get the first result. It could be already visited into the
// dynamic mode and we should skip it.
if (recordsPresent && adapter.first())
{
Object[] res = adapter.get();
for (int j = 0; j < res.length; j++)
{
Long id = res[j] instanceof Record
? ((Record) res[j]).primaryKey()
: (Long) res[j];
if (!ids[j].equals(id))
{
// We didn't visit this result, reset to the original position.
adapter.reset();
break;
}
}
}
}
// Preselect --> Dynamic transition.
else
{
// Allows execute() to determine that we need a new DynamicResults object.
adapter.setResults(null);
// Retrieve dynamic results. Adapter will be reconnected to new delegate results.
execute();
}
}
}
}