BufferManager.java
/*
** Module : BufferManager.java
** Abstract : Coordinates life cycle of record buffers
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050926 @23246 Created initial version. Manages multiple
** record buffers within each context. Maps
** legacy transactions to database-level
** transactions and registers buffers for commit
** and rollback notifications.
** 002 ECF 20051108 @23376 Adjust to new architecture of one Persistence
** instance per database. Most changes were in
** the inner class DBTxWrapper.
** 003 ECF 20051205 @23670 Renamed method obtain() to get() for
** consistency within package.
** 004 ECF 20060116 @23950 Fixed regression in initializeTransactions.
** Was impossible to begin a new database
** transaction.
** 005 ECF 20060120 @24037 Integration with P2J server. Replaced
** ThreadLocal with ContextLocal.
** 006 ECF 20060130 @24285 Changed openScope signature. Now returns
** current scope depth.
** 007 ECF 20060205 @24286 Implement BatchListener and register with the
** TransactionManager for batchNotify events.
** Triggers start/end of record buffer batch
** assign mode.
** 008 ECF 20060428 @25869 Fixed dangling transaction defect. A database
** level which is not explicitly committed or
** rolled back by the TransactionManager is now
** closed by auto-rollback processing.
** 009 GES 20060517 @26197 Match new Finalizable interface requirement.
** 010 ECF 20060610 @27090 Fixed retry defect. A new database level
** transaction must be opened upon master
** transaction retries. This prevents a later
** commit or rollback from failing.
** 011 ECF 20060621 @27503 Added activeRecords method. Returns iterator
** on the records currently stored in all record
** buffers in the current client context for a
** particular database.
** 012 ECF 20060627 @27587 Modified activeRecords method. Only include
** non-transient records to avoid saving records
** which were not meant to be saved.
** 013 ECF 20060628 @27668 Added getCurrentScope method. Reports current
** buffer scope level.
** 014 ECF 20060719 @28110 Simplified activeRecords() implementation.
** 015 ECF 20060728 @28263 Moved DMO reference counting mechanism from
** RecordBuffer. incrementDMOUseCount() and
** decrementDMOUseCount() are now implemented
** here.
** 016 GES 20060804 @28431 Some javadoc improvements.
** 017 ECF 20060916 @29664 Added DMO property-level undo support. A
** scoped dictionary tracks all changes by DMO
** type, necessary to undo changes to any block
** scope. Also moved storage of record-level
** reversible actions from RecordBuffer class.
** 018 ECF 20060919 @29695 Fixed DMO eviction. The DMO implementation
** class must be passed to decrementDMOUseCount,
** because there previously was not enough
** information available to detect the proper
** DMO type when evicting objects from the
** persistence session.
** 019 ECF 20061010 @30307 Added support for auto-commit temp tables.
** Buffers which auto-commit are not managed by
** this class' transaction mechanism.
** 020 ECF 20061011 @30355 Rewrote logic to begin database transactions.
** Eliminated initializeTransactions() method
** and simplified scopeStart() and openScope()
** logic. This change closes some loopholes
** which would not have begun database level
** transactions at the correct time.
** 021 ECF 20061018 @30486 Moved processing to clean up batch assign
** mode from RecordBuffer. This was interfering
** with end-of-scope processing in some cases.
** 022 ECF 20061114 @31159 Moved batch assign mode state management from
** RecordBuffer to this class. Added package
** private methods to allow RecordBuffer access
** to batch mode state.
** 023 ECF 20061207 @31618 Fixed DMO validation problem. ChangeBroker
** needs to be marked for a pending session
** flush whenever a record buffer is added to
** the set of dirty buffers in batch assign
** mode.
** 024 ECF 20070130 @32037 Fixed handling of no-undo temp-tables. Change
** undone with a database-level transaction
** rollback is re-rolled forward and committed,
** as if the rollback had not occurred.
** 025 ECF 20070412 @32976 Fixed end of transaction lock processing.
** Record locks for a session are now managed by
** RecordLockContext.
** 026 ECF 20070414 @32993 Fixed memory leak. DBTxWrapper was holding on
** to record buffers after they had gone out of
** scope. The new locking architecture has
** eliminated the need for this object to hold
** buffer references at all.
** 027 ECF 20070502 @33368 Added lookupBuffer(). Required changes to how
** scoped dictionary for open buffers is managed
** internally.
** 028 ECF 20070503 @33390 Added code to track all defined buffers in a
** scoped dictionary. Added registerBuffer()
** method to allow new buffers to register
** themselves. Changed lookupBuffer() to use the
** new dictionary of all buffers rather than
** just open buffers.
** 029 ECF 20070515 @33668 Optimized startScope() slightly. Only process
** DBTxWrapper if beginning a full transaction.
** 030 ECF 20070601 @33916 Fixed potential memory leak. Release all
** reversible rollbacks at the end of a database
** transaction, rather than only at rollback.
** 031 ECF 20070821 @34901 Added setSilentError() method. Enables error
** suppression during query construction and/or
** initialization to mimic Progress' lenient
** error handling during its processing of a
** where clause.
** 032 ECF 20070911 @35283 Replaced database name string with Database
** information object. Required for non-local
** database connections.
** 033 ECF 20071024 @35594 Added call to allow ConnectionManager to
** clean up after a transaction ends. This was
** necessary to avoid this cleanup too early in
** the end-of-transaction logic flow. This
** problem was possible with the previous
** implementation, in which ConnectionManager
** registered itself with the TM independently.
** 034 ECF 20071029 @35632 Changed in[de]crementDMOUseCount() signature.
** Return use counts after change is made. Also
** integrated some use of generics.
** 035 ECF 20080516 @38600 Added support for dirty index update sharing.
** Integrated generics.
** 036 ECF 20080606 @38637 Fixed reversible rollback processing. Ensure
** that it only takes place when rolling back
** temp table transactions.
** 037 SVL 20080607 @38638 Fixed DBTxWrapper.iterate(). This method
** needs to perform cleanup processing for the
** master transaction by calling end() before
** begin(). [ECF: same change made for retry()].
** 038 CA 20080808 @39357 Implemented no-undo temp-table rollback - on
** db tx rollback, the re-roll actions are
** processed for the no-undo buffers.
** 039 CA 20080826 @39568 Added rollbackPending which is called by TM
** when rollback targets a parent block.
** 040 ECF 20080827 @39601 Fixed lookupBuffer(). Addition of
** pendingBuffers logic regressed this method.
** 041 CA 20080820 @39451 Implemented DatabaseConnectionHandler - it
** checks if an exception related to database
** connectivity errors should be re-thrown or
** not. Fixed leak in allBuffers scoping.
** 042 ECF 20080925 @39945 Modified for ReversibleChain API change. That
** class' c'tor requires an additional argument.
** 043 ECF 20081020 @40207 Modified for Reversible API change. Method
** Reversible.rollback() now requires arguments.
** 044 ECF 20081118 @40548 Fixed reversible rollback processing in
** DBTxWrapper.rollback(). Needed a loop before
** actual rollback, to call Reversibles'
** prepareRollback() method.
** 045 ECF 20081127 @40711 Implement reclamation of temp table primary
** keys. On full transaction commit/rollback,
** keys which have been collected for recycling
** are made available for re-use, or discarded,
** respectively.
** 046 SVL 20081204 @40795 Added nestedOpen parameter to openScope()
** method.
** 047 ECF 20090114 @41161 Minor change to DBTxWrapper. begin() is now
** called from scopeStart() after all buffers
** have been processed. It is called from
** openScope() for a newly created DBTxWrapper.
** 048 ECF 20090121 @41193 Fixed defect in scopeStart()/scopeFinished().
** Buffers were having their enterBlock() and
** exitBlock() methods (respectively) invoked
** too often in the case of nested, open buffer
** scopes.
** 049 ECF 20090128 @41238 Changed activeRecords() to activeBuffers().
** Now returns an iterator on all active record
** buffers, rather than just the records they
** contain. The caller required additional
** information stored by the buffers themselves.
** 050 ECF 20090206 @41265 Added support for evicting unused DMOs in the
** P2J session interceptor. Exposed a view of
** DMOs currently in use so that the session
** interceptor can evict unused DMOs after a
** session flush. Also implemented a minor
** performance improvement for batch mode. We
** now pass the BufferManager instance to
** RecordBuffer.cleanupBatchMode() method to
** avoid an unnecessary context-local lookup.
** 051 ECF 20090218 @41330 Changed idiom for scoped maps of Reversibles.
** The get*() methods which return these maps
** will now return null if the map did not
** previously exist, and the caller did not set
** the create option to true. This avoids the
** unnecessary creation of maps in the common
** case of looking up an entry which will never
** be found.
** 052 ECF 20090221 @41444 Added removeAllReversibles(). Also removed
** unused parameter from decrementDMOUseCount().
** 053 ECF 20090315 @41608 Fixed NO-UNDO temp-table reversible rollback
** processing. It is necessary to prepare the
** reversibles for rollback before the old
** session is rolled back. The reversals are
** then executed in the new session. This allows
** us to initialize lazy persistent collections
** for extent fields before the old session is
** gone. This is a corner case for reversible
** deletes when the target DMO is also the
** current record in the buffer at the time of
** rollback.
** 054 ECF 20090610 @42632 Added evictDMOIfUnused(). Evicts DMO from
** Hibernate session if no resource references it.
** 055 ECF 20090623 @42962 Simplified internals and reduced memory
** footprint. Refactored four ScopedDictionary
** instances for reversible/undoable processing into
** one ScopedDictionary of UndoableData objects.
** Simplified API to access reversible/undoable
** action maps.
** 056 ECF 20090701 @43010 Fixed regressions introduced with #055 (@42962).
** getReversibles() and getUndoables() were not
** properly looking up or creating entries in the
** backing ScopedDictionary.
** 057 ECF 20090702 @43027 Fixed additional regressions from #055 (@42962).
** Reversible rollbacks were not being processed
** correctly.
** 058 ECF 20090716 @43213 Enabled record buffers to be lazily initialized.
** This required some refactoring with respect to
** scope open/close logic. Also ensure change is
** flushed to the database at the close of each
** block.
** 059 ECF 20090724 @43415 Fixed reversible/undoable mapping. Use master
** buffer instead of current buffer as map key. This
** avoids losing undo information for shared temp
** table buffers which go out of scope before their
** master temp tables. Also changed session flush
** strategy. We now try to flush before rollback at
** any level, rather than on scope finish events.
** 060 ECF 20090729 @43447 Adapted Commitable API change. Added validate()
** method to Commitable implementations.
** 061 CA 20090807 @43556 Reversibles must be kept by database and table
** name, not by RecordBuffer instance - to ensure
** proper reversible order when rolling back records
** in a table with multiple buffers defined for it.
** This fixes @43542.
** 062 ECF 20090810 @43582 Changed getBufferForEntity() to prevent NPE. This
** is needed, as RecordBuffer.getDatabaseAndTable()
** will now return null instead of throwing an
** IllegalStateException if the buffer is not yet
** initialized. Throw RuntimeException in various
** places (rather than a ConditionException), if an
** error is unrecoverable.
** 063 ECF 20090811 @43586 Changed decrementDMOUseCount() to prevent NPE.
** In a case where the thread is interrupted due to
** a session ending, it was possible for the
** increment and decrement calls to become out of
** balance.
** 064 ECF 20090811 @43598 Fixed regression in #061 (@43556). Reversibles
** were being stored by database and table name, but
** retrieved by RecordBuffer instance.
** 065 ECF 20090812 @43602 Changed Reversible key from String to new class
** BufferType. Removed getBufferForEntity(), which
** would fail to find a matching buffer in cases
** where buffers had their scopes closed before a
** transaction ended. A buffer lookup is no longer
** needed, since BufferType contains all information
** needed by former callers of that method.
** 066 ECF 20090816 @43665 Integrated with FieldAssigner. We get a context
** local Scopeable from that class, which we notify
** at each scope open and close event.
** 067 SVL 20091117 @44401 Added facility for releasing buffers into which a
** new record was loaded (or the old record was
** reloaded) at specific points of block processing.
** 068 CA 20091202 @44466 Reworked the rollback mechanism, to maintain
** proper reversible order and thus to solve some
** unique-constraint violations during rollback of
** some bulk delete cases and mixed record operations.
** 069 SIY 20100429 @44838 Fixed double rollback in some conditions.
** 070 CS 20121031 Removed client specific references.
** 071 OM 20130902 Added getDMOUseCount() for detecting if a dmo is referred by more
** than one record buffer.
** 072 HC 20130902 For #2164 added BufferManager initialization check.
** BufferManager is initialized when its initialize()
** method is called. If not initialized get() throws.
** 073 SVL 20130822 Added openFreeScope.
** 074 ECF 20131028 Made class and getCurrentScope() public and added import statement to
** accommodate new lock sub-package.
** 075 SVL 20131028 Added openScopeAt and tracking functions for static temp tables.
** 076 CA 20131209 Added no-op deleted() and scopeDeleted() methods, requried by the
** changes in Scopeable and Finalizable interfaces.
** 077 SVL 20140124 Added includeTransient parameter for activeBuffers() function.
** 078 SVL 20140221 [INPUT-]OUTPUT TABLE-HANDLE parameters are tracked.
** 079 CA 20140301 Allow buffers to survive the persistent procedure's external
** procedure.
** 080 OM 20140417 Fixed typo in method name. Simplified string concatenation
** operations.
** 081 CA 20140407 Fixed scopeDeleted(), when it is called on deletion of an appserver
** persistent procedure.
** 082 CA 20140513 Added a weight for the context-local var, to ensure predetermined
** order during context reset.
** 083 ECF 20140813 Manage batch assign brackets in a scoped manner, since now these
** brackets can be nested (requirement of database triggers). Replaced
** Apache commons logging with J2SE logging.
** 084 ECF 20150124 Lazily initialize ConnectionManager to avoid premature use of
** persistence services during server startup.
** 085 ECF 20150220 Rewrote unique buffer name algorithm to ensure names are unique among
** all buffers in the current context, but are consistently reused for a
** given base, legacy name. This allows the dynamic query cache to
** operate more efficiently.
** 086 CA 20150312 Fixed cleanup of tracked buffers; scope notifications are now
** properly raised when the external procedure gets terminated.
** 087 CA 20150320 Backed out H086 - it caused a regression and the change is not part
** of the root mem leak cause.
** 086 ECF 20150322 Partially rewrote unique buffer algorithm introduced with H086 to not
** use String.format(), which is very inefficient. Added deregistration
** of dynamic buffers to prevent leakage of tracked, dynamic buffers.
** Made other performance improvements.
** 087 ECF 20150327 Enhanced logging to determine source of a conflicting transaction
** when attempting to begin a new one.
** 088 ECF 20150906 Reimplemented implicit silent error mode for query substitution
** parameter processing. Simplified and optimized in-use DMO tracking.
** 089 ECF 20150929 Optimized unused DMO eviction.
** 090 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 091 OM 20160212 Added cleanupPending() method. Finalizable API has changed. Upgrade
** code to Java8.
** 092 ECF 20160321 Replaced ScopedDictionary with more performant ScopedList for
** openBuffers data structure. Fixed memory leak in scopeDeleted. Fixed
** removeAllReversibles to remove reversibles from parent scope as well
** as current scope.
** 093 ECF 20160429 Optimized removeAllReversibles. Added toString to RefCount.
** 094 ECF 20160605 Replace TM.registerCommit with TM.registerCommitAt, to avoid extra
** context-local dereferencing in TM.
** 095 ECF 20160823 Fixed primary key reclamation algorithm. Unfortunately, it was later
** deemed that the reclamation concept itself seems inherently unsafe,
** because of the potential for deadlocking due to out-of-order keys.
** However, I want to keep this code around in case we figure out a way
** to use this safely in the future.
** 096 ECF 20160829 Performance enhancement: track transaction status so other objects
** in this context can use this local resource to check transaction
** status instead of TransactionManager.
** 097 GES 20171207 Removed silent error mode processing.
** 098 CA 20180619 DBTxWrapper notifications are DB-specific, so
** ConnectionManager.transactionEnd must be informed of the database
** being processed.
** 099 CA 20181112 Some refactoring required to explicit begin or end a tx, usable by
** the TRANSACTION:SET-COMMIT/ROLLBACK() method implementations.
** 100 OM 20181031 Added BufferManager-level unknown mode support.
** 101 OM 20190111 Avoided circular indirect recursivity at initialization when _Connect
** meta-table is active.
** 102 HC 20190206 Fixed a potential NPE in buffer processing during an error
** condition.
** OM 20190309 Mapped StaticTempTables by a key formed from the legacy name and the
** This-Procedure to avoid collisions.
** ECF 20190313 Extracted RefCount inner class to a top level class.
** ECF 20190322 Refactored to make this class Commitable, to reduce the number of
** individual registrations with the TransactionManager, and to
** optimize notifications of transaction events to open buffers.
** CA 20190325 Implemented ProcedureHelper instead of direct usage of the static
** API. This allows the elimination of context local usage in
** ProcedureManager.
** ECF 20190325 Replaced TransactionManager static calls with TransactionHelper
** instance calls to reduce context-local lookups.
** 103 ECF 20190414 Track records currently being deleted by buffer type and primary key.
** 104 ECF 20190714 Added support for aggressive buffer flushing mode.
** ECF 20190725 Filter "fake" write trigger old buffers from those returned by
** activeBuffers.
** ECF 20190805 Prevent an IllegalStateException when a master iterate call tries to
** begin a transaction immediately after the DBTxWrapper c'tor just did
** so, without an intervening commit or rollback. This can happen if the
** object is constructed after commit processing, during iterate
** processing, by schema trigger business logic.
** ECF 20190813 Change to RecordBuffer.release API.
** 105 CA 20190718 Lookup a table in the entire stacktrace, not just current program.
** CA 20190722 Allow lookup for handles registered as OUTPUT arguments.
** CA 20190724 As the legacy field names are emitted in the same case as they were
** defined, the TempTableKey must be case-insensitive.
** ECF 20190725 Filter "fake" write trigger old buffers from those returned by
** activeBuffers.
** OM 20190802 Added registry for looking up (static) buffers by their legacy name.
** CA 20190816 Static temp-tables are now created when their default buffer is
** defined.
** 106 CA 20191013 Added where and sort clause translation in case of bound buffers.
** 107 AIL 20191119 Transaction rollback should be done only if a transaction is active.
** CA 20191119 Track the executing legacy class, to be able to register any buffer
** to its source definition class.
** CA 20200110 Fixed issues with buffer registration for OE class hierarchy.
** 108 CA 20200323 A fix for binding a source buffer's DMO alias to the destination: this
** must be permanent only in case of BIND arguments. Otherwise, the
** DMO alias binding is limited only for the duration of that top-level
** block call.
** 109 ECF 20200419 New ORM implementation.
** CA 20200722 Performance improvements.
** CA 20200827 Changed pendingActiveDatabases to activeDatabases map to count the open buffers
** for a certain database.
** 110 AIL 20200616 Added pm notification when a static record buffer is changed / reloaded.
** AIL 20200622 The pm should also be notified of temporary record buffer changes.
** ECF 20200909 Treat resetState as an undo operation and do not flush a buffer on release.
** CA 20200921 Avoid NPE in deregisterBindingBuffer if the buffer was already deregistered (as
** it closed its scope).
** CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class.
** CA 20201003 Replaced Guava identity HashSet with Collections.newSetFromMap(IdentityHashMap).
** Allow dictionaries with an identity map.
** OM 20201231 Fixed buffer state in resetState().
** AIL 20210319 Commit instead of rollback no-undo tables.
** ECF 20210414 Changed scope transition behavior to only update scoped data structures when
** entering or leaving block scopes which are either outside an application level
** transaction or, if within a transaction, which have transaction block properties.
** OM 20210429 Release the current record at the end of the buffer scope and decrement the DMO
** use count to allow the persistence to evict it if not in use.
** ECF 20210504 Fixed incorrect block depth used to register callbacks with the transaction
** manager. Refactored begin/end transaction methods.
** AIL 20210507 Allow lookupByName search deeper for a buffer by its legacy name.
** ECF 20210519 Removed DMO use counting. Reference count is now maintained by BaseRecord.
** ECF 20210606 Removed short circuit check in scopeDeleted, which could potentially cause a
** memory leak by bypassing resource cleanup.
** IAS 20210613 Get rid of the permanent connections notion.
** ECF 20210806 Fixed nested query error detection/reporting.
** ECF 20210924 Removed tracking of records currently being deleted. This is managed within
** RecordBuffer and BaseRecord now.
** OM 20211006 Unbinding occurs on procedure return instead of TM's scope finished.
** AL2 20211015 Use real referents instead of class when registering buffers with byLegacyName.
** CA 20211025 pendingStaticTempTables must be emptied on cleanupPending().
** ECF 20211122 Added a global parameter to openScope, to allow registration of persistent
** procedure buffers at the global scope of the openBuffers scoped list.
** ECF 20211124 Rolled back previous fix. Ensure the correct transition of a persistent
** procedure buffer from uninitializedBuffers to openBuffers when that buffer is
** initialized.
** OM 20211201 Improved transaction support for finally blocks.
** CA 20211222 Fixed memory leak - cleanup empty map values from byLegacyName map.
** CA 20220104 Fixed memory leak - when deleting a buffer, remove it from any 'persistent
** procedure scope', too.
** CA 20220105 Fixed byLegacyName lookup - the buffers must not be moved to global scope, but
** instead the lookup must use both the stack buffers (for i.e. internal procedures)
** and the buffers defined at the internal procedure's this-procedure.
** CA 20220206 Each buffer knows the scope(s) at which it was registered with the allBuffers,
** openBuffers, loadedBuffers and byLegacyName registries, so that the processing
** during delete (at deregisterDynamicBuffer) will target explicit scopes, and will
** avoid iterating all scopes.
** Added 'dirtyBuffers' scoped dictionary, to track any buffer which had a Create,
** Update or Delete operation in the current scope. The scope is cleared only when
** the transaction block is processed, otherwise buffers are merged to previous
** scope, but only if their record is actual 'dirty'.
** CA 20220210 Removed Commitable.rollbackPending and its notifications, as there is no actual
** implementation of this method.
** CA 20220308 Fixed scopeFinished(), the condition to not merge dirty buffers to global block
** was incorrect.
** TW 20220617 Use new TailMap for dirtyBuffers. This addresses an issue where new dirty buffers
** were added to the collection during iterating it, resulting in a
** ConcurrentModificationException (refs #6356).
** CA 20220707 Cache the converted Java names for the dynamic buffers.
** 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 20220906 Removed state which is no longer used (PersistentProcScope.loadedBuffers and
** undoData) and removed methods which are not being invoked.
** Moved the parameter assigner scopeable support to AbstractParameter (as all
** parameters require scope support, not just fields).
** Extracted all BufferManager state which requires notification for
** 'isImportantBlock' to an external TxWrapper class (including the private class
** here), to allow the scopeable BufferManager state related only to buffers (and
** not transaction related) to be registered for scope notifications in a lazy
** manner, when buffers are accessed/used/opened.
** CA 20221006 An improvement in deregisterBindingBuffer, do not use entrySet() iterator if both
** key and value are not required. Refs #6821
** CA 20221006 The 'nearestExternal' support needs to be in BufferManager, as it is used for
** computing a scope in ChangeBroker - BufferManager is registered for external
** block scope.
** CA 20221010 Performance improvements - avoid the ProcedureData lookup, by keeping a parallel
** stack of this data for THIS-PROCEDURE. Refs #6826
** CA 20220526 When a persistent program gets deleted, remove all dirty buffers associated with
** it, from all scopes.
** CA 20220601 Nested batch assign mode is allowed in internal mode.
** CA 20220613 registerBindingBuffer must use as procedure the SOURCE-PROCEDURE in case of
** OUTPUT and THIS-PROCEDURE in case of INPUT, for BIND parameters.
** CA 20220918 Do not create a procedure scope and cache it, if there is no buffer state to
** cache.
** CA 20220919 PersistentProcScope.openBuffers can be an identity map, to allow better control
** over the remove.
** CA 20221006 The 'nearestExternal' support needs to be in BufferManager, as it is used for
** computing a scope in ChangeBroker - BufferManager is registered for external
** block scope.
** CA 20221010 Performance improvements - avoid the ProcedureData lookup, by keeping a parallel
** stack of this data for THIS-PROCEDURE. Refs #6826
** CA 20230104 Keep a reverse mapping of bound buffers, per each external program, to avoid
** iterating the entire set, when a scope is started.
** CA 20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD
** runtime.
** HC 20230118 Eliminated some of the uses of String.toUpperCase and/or
** String.toLowerCase for performance.
** 111 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 112 AL2 20230613 Allow retrieving the ProcedureHelper with this manager to avoid context look-ups.
** 113 DDF 20230608 Made the size of the convertedNames LRUCache configurable.
** DDF 20230613 Removed the static block that reads the configuration size of the cache.
** CacheManager will handle the finding of the configuration size and the
** cache creation.
** DDF 20230627 Made the cache final.
** DDF 20230706 Replaced createLRUCache call with another one that will use a null
** discriminator by default.
** 114 CA 20230724 Further reduce context-local usage.
** 115 CA 20231026 A persistent procedure with a pending delete is no longer persistent, so
** scopeDeleted needs to be executed.
** CA 20231031 Added 'BlockDefinition' parameter to 'scopeStart'.
** 116 ASL 20231107 Implemented iterative search for ScopedDictionary key index in case of
** postponed table handles.
** ASL 20231117 Modified externalScope stack to nextExtScope stack to easily access the nearest
** external scope available. Changed the ScopedDictionary index search to a simple
** expression.
** 117 CA 20231216 Avoid processing in maybeCloseSessions if activePersistenceContexts is empty.
** CA 20231221 Avoid lowercasing the name in TempTableKey and use case-insensitive hashcode.
** 118 DDF 20240130 Created isActiveBuffer method.
** DDF 20240130 Inlined condition in isActiveBuffer().
** 119 CA 20240206 If THIS-PROCEDURE is already on the stack, do not re-bind the bound buffers.
** 120 CA 20240302 A BIND at the param or argument has effect only if either the argument has
** BY-REFERENCE or the target is REFERENCE-ONLY. Removed the 'bindBuffer' support
** which sets and unsets the DMO alias as this was determined to be obsolete and/or
** incorrect.
** 121 CA 20240404 Static temp-table or dataset resolution by name must be done using the currently
** executing type and the static temp-table/dataset's defining type.
** 122 AL2 20240617 Made bufferInitialized return a boolean.
** 123 SP 20240801 Extracted byLegacy lambda to deregisterByLegacy() method.
** CA 20240809 Removed capturing lambda from 'setUnknownMode'.
** 124 CA 20240827 Added 'getTxWrapperHelper'.
** 125 OM 20240909 Improved Database API.
** 126 CA 20230924 Further reduce context-local lookup.
** 127 AS 20241016 Refactored dirtyBatchBuffers to a Map<RecordBuffer, Runnable>
** containing the buffers and the code to restore their active state.
** 128 EAB 20241008 Added tableRecordBuffer to track and group buffers that reference the same DMO for
** better organization and lookup.
** AL2 20241023 Created registerToBuffersByClass to de-duplicate code.
** AL2 20241023 Extended uses of buffersByDMOBufClass to work with persistent procs and
** dynamic buffers.
** 129 CA 20241119 A DMO alias must not emit as a FQL reserved keyword.
** 130 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 131 ES 20250130 Change generateUniqueBufferName method to generateBufferName, without adding the suffix
** to the end of the javaname.
** ES 20250205 Added generateUniqueToken method and changed the allBuffers key from
** getDMOAlias() to getDMOToken().
** 132 DDF 20250507 Improve performance by avoiding expensive lookups.
*/
/*
** 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.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.logging.*;
import static java.util.AbstractMap.SimpleEntry;
import com.goldencode.cache.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.id.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.util.*;
import com.goldencode.util.Stack;
/**
* Manages all record buffers within the current user context. This includes
* transaction processing, buffer and block scope management. A separate
* instance of this class is created within each user context. It acts as a
* broker between the context local <code>TransactionManager</code>, the
* persistence service layer which manages database resources, and multiple
* record buffers opened by client code.
* <p>
* A record buffer registers itself with the buffer manager when client code
* opens a new buffer scope. If a transaction is active, that buffer is
* immediately registered with the transaction manager for commit processing.
* This allows the buffer to synchronize its state with the persistence
* service when a transaction or subtransaction commit occurs. If a
* transaction is not active when a new buffer scope is opened, the buffer
* manager keeps track of the open buffer, and the block scope at which it
* was opened. When a full transaction subsequently is opened, all buffers
* with open scopes are batch registered for commit processing.
* <p>
* Also, when a full transaction begins, the buffer manager takes stock of
* all databases referred to by open buffer scopes, and begins a new,
* database-level transaction for each one. The database transaction's life
* cycle is managed by an inner class, {@link TxWrapper}.
* This inner class registers itself for master commit and finish processing
* with the transaction manager, such that it can reflect an application
* level commit/rollback back to the underlying database transaction.
* <p>
* Updates made to existing records are tracked for undo purposes by this
* class. A scoped dictionary of reversible updates by DMO type is managed
* by the context local instance of this class, for all open buffers in the
* current client context.
*/
public final class BufferManager
implements Scopeable,
BatchListener,
StopConditionVetoHandler
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(BufferManager.class.getName());
/** Stores context local instances of this class */
private static final ContextLocal<BufferManager> context = new ContextLocal<BufferManager>()
{
@Override public WeightFactor getWeight() { return WeightFactor.LEVEL_2; }
@Override protected BufferManager initialValue() { return (new BufferManager()); }
};
/** A cache of buffer-related fields defined in a specific legacy-converted class. */
private static final Map<Class<?>, Set<Field>> classBufferFields = new ConcurrentHashMap<>();
/** Map of buffer types to high-water block depths of any change to any record of that type */
// intentionally package private for direct use from RecordBuffer
final Map<BufferType, MutableInteger> changeScopes = new HashMap<>();
/** Finalizable which clears batch mode at the appropriate times */
private final Finalizable batchCleaner = new Finalizable()
{
@Override public void finished() { RecordBuffer.cleanupBatchMode(BufferManager.this); }
@Override public void deleted() { }
@Override public void iterate() { RecordBuffer.cleanupBatchMode(BufferManager.this); }
@Override public void retry() { RecordBuffer.cleanupBatchMode(BufferManager.this); }
};
/** The buffers which will be registered on the next scope entry. */
private final Set<RecordBuffer> pendingBuffers = Collections.newSetFromMap(new IdentityHashMap<>());
/** The defining class of a pending buffer, used when initializing an OE class and/or external program. */
private final Map<RecordBuffer, Class<?>> pendingBufferClasses = new IdentityHashMap<>();
/**
* Scoped dictionary in which all defined record buffers are stored. This data structure uses
* the DMO alias as keys.
* TODO: this can't be by String, otherwise we can't separate the buffers to its defining class
*/
private final ScopedDictionary<String, RecordBuffer> allBuffers = new ScopedDictionary<>();
/**
* Scoped dictionary with key as a DMO buffer interface and multiplex, and the value is a list of
* RecordBuffers associated with them.
*/
private final ScopedDictionary<SimpleEntry<String, Integer>, Set<RecordBuffer>> buffersByDMOBufClass =
new ScopedDictionary<>();
/**
* Scoped dictionary in which all defined record buffers are stored. This data structure uses
* the <strong>lowercased legacy name</strong> as keys.
* <p>
* Note: only the static buffers are kept in this structure. The dynamic resources cannot be
* stored here as their NAME attribute is mutable and will possible collide with other
* existing buffers.
*/
private final ScopedDictionary<Object, Map<String, RecordBuffer>> byLegacyName = new ScopedDictionary<>();
/** Buffers whose scopes have been opened, but are not yet initialized */
private final Map<RecordBuffer, Boolean> uninitializedOpenBuffers = new WeakHashMap<>();
/** Scoped dictionary in which open record buffers are stored */
private final ScopedList<RecordBuffer> openBuffers = new ScopedList<>();
/**
* Scoped dictionary which contains currently active static temp tables (i.e. table scope was
* opened and not yet closed). Keyed by a {@code TempTableKey} compound from the legacy table
* name and the procedure where the temp table was defined.
*/
private final ScopedDictionary<StaticDefinitionKey, StaticTempTable> openStaticTempTables =
new ScopedDictionary<>();
/** The registry with all static {@code Datasets} mapped by their name. */
private ScopedDictionary<StaticDefinitionKey, DataSet> dataSetRegistry = new ScopedDictionary<>();
/** A map of static temp-tables pending registration. */
private final Map<StaticTempTable, Boolean> pendingStaticTempTables = new HashMap<>();
/**
* Scoped dictionary which contains the maps of output copiers for a procedure/function to
* their 0-based indexes in the set of all TABLE-HANDLE parameters for the procedure/function
* (excluding other types of parameters).
*/
private final ScopedDictionary<OutputTableHandleCopier, Integer> thOutputParameters =
new ScopedDictionary<>();
/** Active persistence contexts (those which have an open database session) */
private final Set<Persistence.Context> activePersistenceContexts = Collections.newSetFromMap(new IdentityHashMap<>());
/** Stack of batch mode data */
private final ArrayDeque<BatchModeData> batchModeStack = new ArrayDeque<>();
/** Map with buffer-related data surviving a persistent proc's external procedure.*/
private final Map<Object, PersistentProcScope> persistProcScopes = new IdentityHashMap<>();
/** Primary keys pending reclamation */
private final Map<Persistence, Set<Long>> pendingReclaimedKeys = new IdentityHashMap<>();
/** Helper to use the ProcedureManager without any context local lookups. */
private final ProcedureManager.ProcedureHelper pm = ProcedureManager.getProcedureHelper();
/** Transaction helper */
private final TransactionManager.TransactionHelper txHelper;
/** Tx wrapper helper. */
private TxWrapper.TxWrapperHelper txWrapperHelper;
/** Change broker instance. */
private final ChangeBroker changeBroker;
/** Buffer names already converted. */
private final LRUCache<String, String> convertedNames;
/** Track the external scopes pushed on {@link BufferManager}. */
private final Stack<Integer> nextExtScope = new Stack<>();
/** Connection manager */
private ConnectionManager connMgr = null;
/** Number of nested queries being processed */
private int queryDepth = 0;
/** Unknown mode is {@code true} while evaluating query parameters */
private boolean unknownMode = false;
/** Does a nested query error condition currently exist? */
private boolean nestedQueryError = false;
/** The cached name converter. */
private NameConverter nc = new NameConverter();
/** Counter for ensuring token uniqueness and avoiding expensive lookups. */
private long tokenCounter = 1;
/**
* Constructor; instances of this class are context-local and must be accessed using the static
* {@link #get} method.
*/
private BufferManager()
{
byLegacyName.setIdentityKeys(true);
thOutputParameters.setIdentityKeys(true);
txHelper = TransactionManager.getTransactionHelper();
txHelper.registerBatchListener(this);
txHelper.registerStopVetoHandler(this);
changeBroker = ChangeBroker.get();
convertedNames = CacheManager.createLRUCache(BufferManager.class, 10000);
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "BufferManager created in context " + Utils.describeContext());
}
}
/**
* Retrieve the context-local instance of this class, instantiating it first if necessary.
*
* @return Context-local instance of this class.
*/
public static BufferManager get()
{
BufferManager bm = context.get();
if (bm.connMgr == null)
{
ConnectionManager cm = ConnectionManager.get();
if (bm.connMgr == null)
{
bm.connMgr = cm;
}
else if (bm.connMgr != cm)
{
// we have a problem: two ConnectionManager-s for same context
throw new RuntimeException("Multiple ConnectionManager in same context.");
}
}
if (bm.txWrapperHelper == null)
{
bm.txWrapperHelper = TxWrapper.getHelper(bm);
}
return bm;
}
/**
* Register the {@link BufferManager} instance as for scope notifications at the current block.
*
* @param bufferManager
* The {@link BufferManager} instance.
*/
public static void registerScopeable(BufferManager bufferManager)
{
if (bufferManager == null)
{
bufferManager = get();
}
boolean isInitializing = bufferManager.pm.getInstantiatingExternalProgram() != null;
if (isInitializing)
{
// there are cases like dataset builder where this is called even if we still are in the implicit
// constructor for creating a new class/program.
registerPendingScopeable(bufferManager);
return;
}
// both ChangeBroker and BufferManager need to be added
bufferManager.txHelper.registerBlockScopeable(bufferManager);
bufferManager.txHelper.registerBlockScopeable(bufferManager.changeBroker);
}
/**
* Register the {@link BufferManager} instance as pending scopeable, to be added to the next block.
*
* @param bufferManager
* The {@link BufferManager} instance.
*/
public static void registerPendingScopeable(BufferManager bufferManager)
{
if (bufferManager == null)
{
bufferManager = get();
}
// both ChangeBroker and BufferManager need to be added
bufferManager.txHelper.registerPendingScopeable(bufferManager);
bufferManager.txHelper.registerPendingScopeable(bufferManager.changeBroker);
}
/**
* Register the {@link TxWrapper} scopeable as pending for the next opened block.
*
* @param bufferManager
* The {@link BufferManager} instance.
*/
public static void registerTxScopeable(BufferManager bufferManager)
{
if (bufferManager == null)
{
bufferManager = get();
}
bufferManager.txWrapperHelper.registerPendingScopeable();
}
/**
* Cleanup of pending resources. This is called when the external procedure constructor failed
* to build a new object because of an exception thrown in a member initialization (shared
* frame/table).
*/
public static void cleanupPending()
{
BufferManager bm = BufferManager.get();
bm.pendingBuffers.clear();
bm.pendingBufferClasses.clear();
bm.pendingStaticTempTables.clear();
}
/**
* Register this buffer as 'dirty' in the current scope.
* <p>
* This registration doesn't mean that at the time of {@link TxWrapper#commit}, {@link TxWrapper#rollback},
* or {@link TxWrapper#validate} the buffer's record is still 'dirty' - this is used to limit the set of
* buffers receiving such notifications, to avoid iterating over all {@link #openBuffers}.
*
* @param recordBuffer
* The record buffer.
*/
public void registerDirtyBuffer(RecordBuffer recordBuffer)
{
// although no state in BufferManager is touched when registering a dirty buffer, we need to register
// it with the current block, as there is logic dependent on the 'depth' of the application stack which
// uses buffers
registerScopeable(this);
txWrapperHelper.registerDirtyBuffer(recordBuffer);
}
/**
* Register a pending buffer's defining class.
*
* @param buffer
* The newly constructed buffer instance.
* @param cls
* The Java class associated with an external program or legacy OE class, where it
* was defined.
*/
public void registerPendingBufferClass(RecordBuffer buffer, Class<?> cls)
{
pendingBufferClasses.put(buffer, cls);
}
/**
* Get the defining class for a pending buffer.
*
* @param buffer
* The newly constructed buffer instance.
*
* @return The Java class associated with an external program or legacy OE class, where it
* was defined.
*/
public Class<?> getPendingBufferClass(RecordBuffer buffer)
{
return pendingBufferClasses.get(buffer);
}
/**
* Resolve the defining Java class for all Java fields which define a buffer.
*
* @param referent
* The instance associated with an external program or legacy OE class.
*/
public void resolvePendingBufferClasses(Object referent)
{
// TODO: this may be deprecated as we need this right after buffer instantiation, and not
// after class initialization
// process any buffer fields and save their classes
Class<?> cls = referent.getClass();
while (cls != BaseObject.class)
{
Set<Field> fields = classBufferFields.get(cls);
if (fields == null)
{
fields = new HashSet<>();
Field[] allFields = cls.getDeclaredFields();
for (Field f : allFields)
{
if (Modifier.isStatic(f.getModifiers()))
{
continue;
}
if (Buffer.class.isAssignableFrom(f.getType()))
{
fields.add(f);
}
}
classBufferFields.put(cls, fields);
}
for (Field f : fields)
{
try
{
f.setAccessible(true);
BufferReference b = (BufferReference) f.get(referent);
registerPendingBufferClass(b.buffer(), cls);
}
catch (IllegalArgumentException | IllegalAccessException e)
{
throw new RuntimeException(e);
}
finally
{
f.setAccessible(false);
}
}
cls = cls.getSuperclass();
}
}
/**
* Query the scope level which currently is open. This is the number of "important" scopes;
* NO_TRANSACTION blocks inside an application transaction are ignored. So, this number may be lower
* than the actual number of nested block scopes open, as tracked by the {@code TransactionManager}.
*
* @return Number of "important" buffer scopes open, by the above definition.
*/
public int getOpenBufferScopes()
{
return openBuffers.scopes();
}
/**
* Find the nearest external block (from the bottom of the stack).
*
* @return See above.
*/
public int findNearestExternal()
{
return nextExtScope.empty() ? -1 : nextExtScope.peek();
}
/**
* Check the specified exception and determine whether we want to allow it
* to be honored by the {@link TransactionManager}, or whether we want to
* veto the STOP at this scope.
* <p>
* This implementation is interested only in STOP conditions which have
* been generated by database connection errors. STOP processing at the
* current scope is vetoed iff the exception is an instance of {@link
* DatabaseConnectionException} AND either of the following criteria are
* met:
* <ul>
* <li>the failing database is one that should be permanently connected
* to this P2J server instance; OR
* <li>there is any record buffer still in scope, which is associated
* with the failing database.
* </ul>
* <p>
* Otherwise, normal STOP processing is allowed to continue.
*
* @param exc
* The exception to be checked.
*
* @return <code>true</code> if STOP should be vetoed;
* <code>false</code> if the STOP should be allowed.
*/
public boolean vetoStop(StopConditionException exc)
{
// If the exception was not caused by a database connection problem, it
// it is not in our domain to veto; allow STOP processing to continue.
if (!(exc instanceof DatabaseConnectionException))
{
return false;
}
DatabaseConnectionException dce = (DatabaseConnectionException) exc;
Database database = dce.getDatabase();
// on database connection errors, the exception should be re-thrown if this is a virtual database
// and this block or a parent block uses a table from this database
for (RecordBuffer buffer : allBuffers.values())
{
// a buffer is in use, the STOP must be vetoed
if (buffer.isActive() && database.equals(buffer.getDatabase()))
{
return true;
}
}
return false;
}
/**
* Executes logic to explicitly begin a transaction.
*
* @param inTx
* Flag indicating if we are in a transaction.
* @param fullTx
* Flag indicating if we are in a full transaction.
* @param blockDepth
* The current block depth (all blocks tracked by the {@code TransactionManager}).
*/
public void beginTx(boolean inTx, boolean fullTx, int blockDepth)
{
txWrapperHelper.beginTx(inTx, fullTx, blockDepth);
}
/**
* Executes logic to explicitly end a transaction.
*
* @param inTx
* Flag indicating if we are in a transaction.
* @param fullTx
* Flag indicating if we are in a full transaction.
*/
public void endTx(boolean inTx, boolean fullTx)
{
txWrapperHelper.endTx(inTx, fullTx);
}
/**
* Executes logic required after a full transaction has ended.
*/
public void endTxPost()
{
// if any DBs were used in this tx block and remained connected, disconnect now
txWrapperHelper.endTxPost();
}
/**
* Process a notification that a new block scope has been entered. Notify
* every buffer with an open scope that a new block scope has started. If
* the new block scope is within the context of a transaction, each buffer
* with an open scope is registered for commit processing with the
* transaction manager. If the block scope represents the opening of a
* new, full transaction, we also initialize database transactions for
* each database referenced by a buffer with an open scope.
*
* @throws ErrorConditionException
* if there is an error flushing pending changes to the database
* as we open a new database transaction.
*
* @param block
* The explicit block definition which required this notification.
*/
@Override
public void scopeStart(BlockDefinition block)
{
if (txHelper.isExternalBlock())
{
nextExtScope.push(nextExtScope.size());
}
else
{
nextExtScope.push(nextExtScope.empty() ? -1 : nextExtScope.peek());
}
allBuffers.addScope(null);
byLegacyName.addScope(null);
buffersByDMOBufClass.addScope(null);
openBuffers.pushScope();
openStaticTempTables.addScope(null);
dataSetRegistry.addScope(null);
thOutputParameters.addScope(null);
BatchModeData crtBatchScope = batchModeStack.peek();
if (crtBatchScope != null && crtBatchScope.batchDepth == BatchModeData.INTERNAL)
{
// normally a new scope cannot be opened inside an INTERNAL batch mode. Except for the OO property
// setters that can be invoked so that the output value to be set
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.INFO, block.type + " pushed on batch assign on top of an INTERNAL scope.");
}
}
batchModeStack.push(new BatchModeData());
if (!pendingStaticTempTables.isEmpty())
{
// always before pendingBuffers
for (Map.Entry<StaticTempTable, Boolean> entry : pendingStaticTempTables.entrySet())
{
registerStaticTempTable(entry.getKey(), entry.getValue());
}
pendingStaticTempTables.clear();
}
if (!pendingBuffers.isEmpty())
{
for (RecordBuffer buffer : pendingBuffers)
{
Class<?> def = pendingBufferClasses.get(buffer);
if (def == null)
{
def = pm._thisProcedure().getClass();
}
registerBuffer(buffer);
}
pendingBuffers.clear();
pendingBufferClasses.clear();
}
// Ensure that batch assign mode is turned off properly, regardless of
// whether business logic invokes RecordBuffer.endBatch(). This
// prevents a nesting error raised during RecordBuffer.startBatch(), if
// a batch is interrupted and then a new one begins.
txHelper.registerFinalizable(batchCleaner, false);
int blockDepth = txHelper.getNestingLevel();
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER,
Utils.describeContext() + ": block scope " + blockDepth + " started");
}
}
/**
* Process a notification that a scope is about to be exited. Notify each
* record buffer with an open scope that the current block scope is ending.
*/
@Override
public void scopeFinished()
{
PersistentProcScope ppScope = null;
Object referent = null;
if (!txHelper.isGlobalBlock())
{
BlockType bt = txHelper.getBlockType();
// if we have a persistent procedure, save the data
if (bt == BlockType.EXTERNAL_PROC && pm.isThisProcedurePersistent())
{
referent = pm._thisProcedure();
// in case of legacy OE classes, and when this is executed from a super-class 'execute'
// surrogate, this will already exist
ppScope = persistProcScopes.get(referent);
if (ppScope == null)
{
ppScope = new PersistentProcScope();
persistProcScopes.put(referent, ppScope);
}
}
}
if (ppScope != null)
{
// save the data for this external procedure, as these will need to be removed from the
// global scope, when the procedure gets deleted.
List<RecordBuffer> thisOpenBuffers = openBuffers.sublistAtScope(0);
ppScope.openBuffers.addAll(thisOpenBuffers);
Map<String, RecordBuffer> thisAllBuffers = allBuffers.getDictionaryAtScope(0, false);
ppScope.allBuffers.putAll(thisAllBuffers);
Map<SimpleEntry<String, Integer>, Set<RecordBuffer>> thisBuffersByDmoClass =
buffersByDMOBufClass.getDictionaryAtScope(0, false);
ppScope.buffersByDMOBufClass.putAll(thisBuffersByDmoClass);
checkLegacyNames(ppScope.byLegacyName, byLegacyName.getDictionaryAtScope(0, false));
Map<StaticDefinitionKey, StaticTempTable> thisOpenStaticTempTables =
openStaticTempTables.getDictionaryAtScope(0, false);
ppScope.openStaticTempTables.putAll(thisOpenStaticTempTables);
Map<StaticDefinitionKey, DataSet> thisDataSets = dataSetRegistry.getDictionaryAtScope(0, false);
ppScope.datasets.putAll(thisDataSets);
// add all these to the global scope, too
if (!thisOpenBuffers.isEmpty())
{
openBuffers.addAll(true, thisOpenBuffers);
for (RecordBuffer buffer : thisOpenBuffers)
{
buffer.addOpenBuffersScope(1); // global scope
}
}
if (!thisAllBuffers.isEmpty())
{
allBuffers.getDictionaryAtScope(allBuffers.size() - 1, true)
.putAll(thisAllBuffers);
}
if (!thisBuffersByDmoClass.isEmpty())
{
buffersByDMOBufClass.getDictionaryAtScope(buffersByDMOBufClass.size() - 1, true)
.putAll(thisBuffersByDmoClass);
}
if (!thisOpenStaticTempTables.isEmpty())
{
openStaticTempTables.getDictionaryAtScope(openStaticTempTables.size() - 1, true)
.putAll(thisOpenStaticTempTables);
}
// datasets are not pushed to global scope
if (ppScope.allBuffers.isEmpty() &&
ppScope.byLegacyName.isEmpty() &&
ppScope.buffersByDMOBufClass.isEmpty() &&
ppScope.openBuffers.isEmpty() &&
ppScope.openStaticTempTables.isEmpty())
{
// if there is no data required for this referent, do not save it.
persistProcScopes.remove(referent);
}
}
else
{
List<RecordBuffer> thisOpenBuffers = openBuffers.sublistAtScope(0);
for (RecordBuffer buffer : thisOpenBuffers)
{
trackDatabase(buffer, false);
}
}
nextExtScope.pop();
openBuffers.popScope();
allBuffers.deleteScope();
byLegacyName.deleteScope();
buffersByDMOBufClass.deleteScope();
openStaticTempTables.deleteScope();
dataSetRegistry.deleteScope();
thOutputParameters.deleteScope();
BatchModeData bmd = batchModeStack.pop();
if (bmd.batchDepth == BatchModeData.INTERNAL)
{
throw new IllegalStateException("Cannot end a scope while an INTERNAL batch is active.");
}
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER,
Utils.describeContext() + ": block scope " + txHelper.getNestingLevel() + " finished");
}
maybeCloseSessions();
}
/**
* Provides notification that the external procedure scope has been deleted.
* <p>
* If we are deleting a persistent procedure, clean all its data from the global scope for the
* following dictionaries: {@link #openBuffers}, {@link #allBuffers}, {@link #openStaticTempTables}.
*/
@Override
public void scopeDeleted()
{
Object referent = pm.getProcessedProcedure();
if (referent == null || pm._isPersistentAndNotPendingDelete(referent))
{
// if no referent or procedure is still persistent, do not clean up yet
return;
}
// TODO: is this in the right place, after the short-circuits above?
maybeCloseSessions();
PersistentProcScope scope = persistProcScopes.remove(referent);
// this needs to be executed only when the procedure gets deleted.
if (scope != null)
{
// remove all these from the global scope
if (!scope.openBuffers.isEmpty())
{
openBuffers.removeAllFrom(true, new ArrayList<>(scope.openBuffers));
for (RecordBuffer buffer : scope.openBuffers)
{
trackDatabase(buffer, false);
}
}
if (!scope.allBuffers.isEmpty())
{
txWrapperHelper.cleanDirtyBuffers(scope.allBuffers);
allBuffers.getDictionaryAtScope(allBuffers.size() - 1, true)
.keySet()
.removeAll(scope.allBuffers.keySet());
}
if (!scope.buffersByDMOBufClass.isEmpty())
{
buffersByDMOBufClass.getDictionaryAtScope(buffersByDMOBufClass.size() - 1, true)
.keySet()
.removeAll(scope.buffersByDMOBufClass.keySet());
}
if (!scope.openStaticTempTables.isEmpty())
{
openStaticTempTables.getDictionaryAtScope(openStaticTempTables.size() - 1, true)
.keySet()
.removeAll(scope.openStaticTempTables.keySet());
}
// datasets are not pushed to global scope
}
}
/**
* Get the {@link ScopeId} for the instance.
*
* @return {@link ScopeId#BUFFER_MANAGER}.
*/
@Override
public ScopeId getScopeId()
{
return ScopeId.BUFFER_MANAGER;
}
/**
* Provides a notification that a batch is starting or ending. Enters or
* exits batch assignment mode for all record buffers in this context,
* depending on the value of <code>start</code>.
*
* @param start
* <code>true</code> if this is the start of a batch, otherwise
* <code>false</code>.
*
* @see RecordBuffer#startBatch
* @see RecordBuffer#endBatch
*/
public void batchNotify(boolean start)
{
if (start)
{
RecordBuffer.startBatch(this, true);
}
else
{
RecordBuffer.endBatch(this, false);
}
}
/**
* Marks the table for postponed delete, if it is possible. Postponed delete can take place if
* the table is used as [INPUT-]OUTPUT TABLE-HANDLE parameter.
*
* @param tempTable
* Temporary table to be marked for postponed delete.
*
* @return <code>true</code> if the table was postponed for deletion. <code>false</code> if
* this can't be done.
*/
public boolean postponeTableHandleDelete(TempTable tempTable)
{
int nearestExt = findNearestExternal();
if (nearestExt != -1)
{
int size = thOutputParameters.size();
Map<OutputTableHandleCopier, Integer> map =
thOutputParameters.getDictionaryAtScope(size - nearestExt - 1, false);
if (map != null)
{
Set<OutputTableHandleCopier> set = map.keySet();
for (OutputTableHandleCopier copier : set)
{
handle outputHandle = copier.getCalled();
if (outputHandle._isValid() && tempTable.equals(outputHandle.getResource()))
{
copier.setTableToDelete(tempTable);
return true;
}
}
// It's not a TABLE-HANDLE parameter, just a dynamic table. Clear all pending
// TABLE-HANDLE parameter deletes which current handle value is not an unknown handle.
for (OutputTableHandleCopier copier : set)
{
if (!copier.getCalled()._isValid())
{
copier.setTableToDelete(null);
}
}
}
}
return false;
}
/**
* Check whether the current block is within an application-level transaction.
*
* @return {@code true} if we are in a transaction, else {@code false}.
*/
public boolean isTransaction()
{
return txWrapperHelper.isTransaction();
}
/**
* Get the number of block scope transitions made. This is meant to be a semaphore of sorts, to allow
* a caller to determine whether this value has changed since some previous point when it was last
* retrieved. Since it is only meant to detect that the current block has changed over time, we do not
* protect against overflow.
*
* @return The current unique block ID.
*/
public long getScopeTransitions()
{
return txWrapperHelper.getScopeTransitions();
}
/**
* Convenient method to retrieve the procedure helper. There are many cases where the buffer manager is
* used and the procedure manager is required. Instead of using this as an intermediate and crowd it with
* gateway methods towards procedure manager, let the user operate directly on the procedure helper.
*
* @return The procedure helper used by this buffer manager.
*/
public ProcedureManager.ProcedureHelper getProcedureHelper()
{
return pm;
}
/**
* Search in all scopes the first record buffer that holds a specific DMO based on a provided PK.
*
* @param entityName
* The name of the entity for which the record buffers are being searched.
* @param multiplex
* Optional multiplex ID (temp-tables only; otherwise {@code null}.
* @param pK
* The primary key used to identify a specific DMO.
*
* @return The first record buffer that matches the search criteria. If none is found,
* this returns {@code null}.
*/
public RecordBuffer getRecordBuffersByDMOBufClass(String entityName, Integer multiplex, Long pK)
{
SimpleEntry<String, Integer> keyByDMOBuf = new SimpleEntry<>(entityName, multiplex);
for (int i = 0; i < buffersByDMOBufClass.size(); i++)
{
Set<RecordBuffer> searchedRecordBuffers = buffersByDMOBufClass.getValueAtScope(keyByDMOBuf, i);
if (searchedRecordBuffers == null)
{
continue;
}
RecordBuffer buffer = searchedRecordBuffers.stream()
.filter(r -> r.getCurrentRecord() != null && r.getCurrentRecord().primaryKey() == pK)
.findFirst()
.orElse(null);
if (buffer != null)
{
return buffer;
}
}
return null;
}
/**
* Get the open buffers from the current scope.
*
* @return See above.
*/
Collection<RecordBuffer> getOpenBuffers()
{
return openBuffers.scopes() == 0 ? Collections.EMPTY_LIST : openBuffers.sublistAtScope(0);
}
/**
* Get the current counter value of application-leve transactions which have been opened.
*
* @return Transaction count.
*/
long getTransactionCount()
{
return txWrapperHelper.getTransactionCount();
}
/**
* Get the {@link TxWrapper transaction wrapper} used by this context.
*
* @return See above.
*/
TxWrapper.TxWrapperHelper getTxWrapperHelper()
{
return txWrapperHelper;
}
/**
* Get the transaction helper object used by this context.
*
* @return Transaction helper.
*/
TransactionManager.TransactionHelper getTxHelper()
{
return txHelper;
}
/**
* Get the {@link #connMgr} instance for this context.
*
* @return See above.
*/
public ConnectionManager getConnMgr()
{
return connMgr;
}
/**
* Register an active persistence context, indicating a database session has been opened.
*
* @param pc
* Persistence context.
*/
void registerPersistenceContext(Persistence.Context pc)
{
activePersistenceContexts.add(pc);
}
/**
* Register this static dataset definition.
*
* @param type
* The definition type.
* @param name
* The legacy name.
* @param ref
* The instance where it is defined.
* @param dataset
* The dataset instance to register.
*/
void registerDataSet(Class<?> type, String name, Object ref, DataSet dataset)
{
if (type == null)
{
LOG.log(Level.WARNING,
"No type specified when registering dataset " + name + " defined in ref " + ref.getClass());
type = pm.getExecuting();
}
StaticDefinitionKey key = new StaticDefinitionKey(name, ref, type);
dataSetRegistry.addEntry(false, key, dataset);
}
/**
* Register a newly defined record buffer in the current scope. Buffers are deregistered
* implicitly when the scope in which they were registered ends.
*
* @param buffer
* Buffer to be registered.
*/
void registerBuffer(RecordBuffer buffer)
{
Object ref = pm._thisProcedure();
registerToBuffersByClass(null, buffer);
allBuffers.addEntry(false, buffer.getDMOToken(), buffer);
buffer.addAllBufferScope(allBuffers.size());
buffer.addAllBufferScope(byLegacyName.size());
Map<Object, Map<String, RecordBuffer>> dict = byLegacyName.getDictionaryAtScope(0, true);
Map<String, RecordBuffer> defBufs = dict.get(ref);
if (defBufs == null)
{
dict.put(ref, defBufs = new CaseInsensitiveHashMap<>());
}
defBufs.put(buffer.getLegacyName(), buffer);
}
/**
* Register as pending the newly defined record buffer. When the next scope
* is entered, all the <code>pendingBuffers</code> will be registered with
* that scope. Buffers are deregistered implicitly when the scope in which
* they were registered ends.
*
* @param def
* The Java class defining this buffer.
* @param buffer
* Buffer pending to be registered.
*/
void registerPending(Class<?> def, RecordBuffer buffer)
{
registerPendingScopeable(this);
pendingBuffers.add(buffer);
registerPendingBufferClass(buffer, def);
}
/**
* Deregister the given, dynamic record buffer by removing it from the buffer manager's
* tracking.
*
* @param buffer
* Dynamic record buffer to be removed.
* @param referent
* The persistent procedure where the buffer was defined; may be <code>null</code>.
*/
void deregisterDynamicBuffer(RecordBuffer buffer, Object referent)
{
String key = buffer.getDMOToken();
String lkey = buffer.getLegacyName();
int openScopes = getOpenBufferScopes();
// these scopes contain the buffer's known scopes where it was registered in the associated dictionaries
// in some cases, this scope may no longer be on the stack or, as the blocks are exited and invoked
// again, the scope on the stack may not be the same as the one on the stack at the time the buffer was
// registered. regardless, the goal of these scopes kept at the buffer is to avoid iterating the entire
// associated dictionaries, and do a 'targeted approach': the buffer knows that it was registered with
// that scope, so it may still be on that scope and thus it needs to be removed.
// also, moving the buffer to the global scope is tracked at this time only for openBuffers, so in the
// other cases it is removed from the global scope, too - as these are dictionaries with identity maps,
// there is no sequential iteration as with openBuffers (which is a ScopedList).
int[] allBufferScopes = buffer.getAllBuferScopes();
int[] byLegacyNameScopes = buffer.getByLegacyNameScopes();
int[] buffersByDmoClassScopes = buffer.getBuffersByDmoClassScopes();
int[] openBuffersScopes = buffer.getOpenBuffersScope();
int scopeSize = allBuffers.size();
for (int scope : allBufferScopes)
{
int depth = scopeSize - scope;
if (depth >= 0)
{
allBuffers.removeEntryAtScope(key, depth);
}
}
// remove from global scope always
allBuffers.removeEntryAtScope(key, -1);
scopeSize = byLegacyName.size();
for (int scope : byLegacyNameScopes)
{
int depth = scopeSize - scope;
if (depth >= 0)
{
Map<Object, Map<String, RecordBuffer>> refs = byLegacyName.getDictionaryAtScope(depth, false);
if (refs != null)
{
deregisterByLegacy(lkey, refs);
}
}
}
// remove from global scope always
Map<Object, Map<String, RecordBuffer>> refs = byLegacyName.getDictionaryAtScope(-1, false);
if (refs != null)
{
deregisterByLegacy(lkey, refs);
}
// remove from buffers by DMO buf class
SimpleEntry<String, Integer> entry =
new SimpleEntry<>(buffer.getDMOBufInterface().getName(), buffer.getMultiplexID());
scopeSize = buffersByDMOBufClass.size();
for (int scope : buffersByDmoClassScopes)
{
int depth = scopeSize - scope;
if (depth >= 0)
{
Map<SimpleEntry<String, Integer>, Set<RecordBuffer>> refs1 =
buffersByDMOBufClass.getDictionaryAtScope(depth, false);
if (refs1 != null)
{
Set<RecordBuffer> buffers = refs1.get(entry);
if (buffers != null)
{
buffers.remove(buffer);
if (buffers.isEmpty())
{
refs1.remove(entry);
}
}
}
}
}
// deregister from global
Map<SimpleEntry<String, Integer>, Set<RecordBuffer>> refs1 = buffersByDMOBufClass.getDictionaryAtScope(-1, false);
if (refs1 != null)
{
Set<RecordBuffer> buffers = refs1.get(entry);
if (buffers != null)
{
buffers.remove(buffer);
if (buffers.isEmpty())
{
refs1.remove(entry);
}
}
}
for (int scope : openBuffersScopes)
{
int depth = openScopes - scope;
if (depth >= 0 && depth < openScopes)
{
openBuffers.removeFrom(depth, buffer);
}
}
// remove from global scope always
// openBuffers.removeFrom(openScopes - 1, buffer);
txWrapperHelper.deregisterDynamicBuffer(buffer);
if (referent != null)
{
PersistentProcScope procScope = persistProcScopes.get(referent);
if (procScope != null)
{
procScope.allBuffers.remove(key);
deregisterByLegacy(lkey, procScope.byLegacyName);
procScope.buffersByDMOBufClass.remove(entry);
procScope.openBuffers.remove(buffer);
}
}
trackDatabase(buffer, false);
}
/**
* Deregister the buffer from all the defined record buffers stored by the lowercased legacy name.
*
* @param lkey
* Lowercased legacy name key
* @param refs
* Scoped dictionary that uses the lowercased legacy name as keys in which
* all defined record buffers are stored.
*/
private void deregisterByLegacy(String lkey, Map<Object, Map<String, RecordBuffer>> refs)
{
Iterator<Map<String, RecordBuffer>> iter = refs.values().iterator();
while (iter.hasNext())
{
Map<String, RecordBuffer> bufDefs = iter.next();
if (bufDefs.remove(lkey) != null && bufDefs.isEmpty())
{
iter.remove();
}
}
}
/**
* Begin batch assignment mode. While in this mode, validation of record buffers is deferred. We remember
* the scope at which this occurs in order to test for errors now and allow proper cleanup later.
*
* @param internal
* Use {@code false} for a programmer required batch assign mode. These can be started at most one
* at each scope depth.<br>
* Use {@code true} for a FWD internal maintenance batch block when some data must be processed
* "in batch". In this case the depth, is not checked. Only one level of this can be added as the
* top-level. This mode must be be ended before starting any new batch.
*
* @see RecordBuffer#startBatch
* @see RecordBuffer#endBatch
* @see RecordBuffer#cleanupBatchMode
*
* @throws IllegalStateException
* if batch mode is already active in the current block or was started but never
* ended in a previous block.
*/
void startBatchAssignMode(boolean internal)
{
registerScopeable(this);
int currentScope = getOpenBufferScopes();
BatchModeData bmd = batchModeStack.peek();
if (!internal)
{
if (bmd == null)
{
throw new IllegalStateException("Internal error: no scopes found. Call scopeStart() first.");
}
if (bmd.batchDepth == currentScope)
{
throw new IllegalStateException("Cannot nest batch assign modes within the same scope");
}
if (bmd.batchDepth != BatchModeData.NOT_ACTIVE)
{
// this should only happen with hand-written or modified code; converted code generally
// will have balanced pairs of start and end batch mode calls
throw new IllegalStateException(
"Batch assign mode was opened but not closed in the same scope (" +
bmd.batchDepth + "; current scope is " + currentScope + ")");
}
bmd.activate(currentScope);
}
else
{
bmd = new BatchModeData();
bmd.activate(BatchModeData.INTERNAL);
batchModeStack.push(bmd);
}
}
/**
* End batch assignment mode if we are currently in batch assignment mode
* and we are in the same scope at which batch assignment mode was
* started. Otherwise do nothing.
*
* @return If the above conditions are met for batch mode to end, the
* map of record buffers which were modified during batch mode is
* returned, with the corresponding code to restore the active state.
* Otherwise, return <code>null</code>, indicating
* either we are not in batch mode, or we are not in the proper
* scope to end it, or the buffers already have been processed, or
* the program is ending and we are in cleanup mode.
*
* @see RecordBuffer#startBatch
* @see RecordBuffer#endBatch
* @see RecordBuffer#cleanupBatchMode
*/
Map<RecordBuffer, Runnable> endBatchAssignMode()
{
BatchModeData bmd = batchModeStack.peek();
Map<RecordBuffer, Runnable> ret = null;
if (bmd == null)
{
// exiting program
return null;
}
if (bmd.batchDepth == BatchModeData.INTERNAL)
{
// the internal scope is removed completely once ended to allow the parent scope to be automatically
// reactivated or to nest other new scopes to be pushed
batchModeStack.pop();
ret = bmd.dirtyBatchBuffers;
}
else
{
if (bmd.batchDepth == BatchModeData.NOT_ACTIVE || getOpenBufferScopes() != bmd.batchDepth)
{
return null;
}
ret = bmd.dirtyBatchBuffers;
bmd.dirtyBatchBuffers = null; // free memory
bmd.batchDepth = -1; // deactivate it
}
return ret;
}
/**
* Indicate whether we currently are in batch assignment mode.
*
* @return <code>true</code> if in batch assignment mode, else
* <code>false</code>.
*
* @see #startBatchAssignMode
* @see #endBatchAssignMode
*/
boolean isBatchAssignMode()
{
return batchModeStack.peek().batchDepth != BatchModeData.NOT_ACTIVE; // this includes INTERNAL mode, too
}
/**
* If we currently are in batch assignment mode, add the given record
* buffer to the set of buffers requiring validation at the end of the
* batch. Otherwise, do nothing.
*
* @param buffer
* Buffer to be added to the set of dirty batch mode buffers.
*
* @param restoreActiveState
* The code that restores the active state of the buffer.
*
* @see #startBatchAssignMode
* @see #endBatchAssignMode
*/
void addDirtyBatchBuffer(RecordBuffer buffer, Runnable restoreActiveState)
{
BatchModeData bmd = batchModeStack.peek();
if (bmd.batchDepth != BatchModeData.NOT_ACTIVE) // this includes INTERNAL mode, too
{
bmd.dirtyBatchBuffers.put(buffer, restoreActiveState);
}
}
/**
* Returns true if a buffer was inserted in the dirtyBatchBuffers map,
* indicating that the setActiveBuffer was already called for it.
*
* @param buffer
* Buffer which should be checked against the dirtyBatchBuffers map.
* @return true if the dirtyBatchBuffers map contains the buffer given as parameter.
*/
boolean isBufferDirtyBatch(RecordBuffer buffer)
{
BatchModeData bmd = batchModeStack.peek();
if (bmd.batchDepth != BatchModeData.NOT_ACTIVE)
{
return bmd.dirtyBatchBuffers.containsKey(buffer);
}
return false;
}
/**
* Increment the query depth.
* <p>
* This method MUST be invoked in balanced pairs that involve a subsequent call to
* {@link #decrementDepth}. Calls may be nested, but they must be balanced, such that
* at the end of all nested calls, <code>queryDepth</code> must be <code>0</code>.
*
* @return <code>true</code> if the starting depth is 0.
*/
boolean incrementDepth()
{
return (queryDepth++ == 0);
}
/**
* Decrement the query depth.
* <p>
* This method MUST be invoked in balanced pairs that involve a subsequent call to
* {@link #incrementDepth}. Calls may be nested, but they must be balanced, such that
* at the end of all nested calls, <code>queryDepth</code> must be <code>0</code>.
*
* @return <code>true</code> if the resulting depth is 0.
*/
boolean decrementDepth()
{
nestedQueryError = false;
return (--queryDepth == 0);
}
/**
* Check whether the current query depth is non-zero, indicating that we are processing a query within
* the context of another query. If so, raise a 7254 error condition, unless the current query represents
* a legacy FIND by RECID/ROWID, which is permitted to run within such context.
*
* @param isFindByRowid
* A lambda expression which should return {@code true} if the current database operation
* represents a legacy FIND by RECID/ROWID, else {@code false}.
*/
void checkNestedQuery(Supplier<Boolean> isFindByRowid)
{
if (queryDepth == 0 || isFindByRowid.get())
{
// normal state
return;
}
Object referent = pm._thisProcedure();
String iename = ProcedureManager.resolveClosestMethod(referent, true, false);
String msg =
"A FIND, FOR EACH or OPEN QUERY statement has been encountered in a user-defined " +
"function, method or constructor '" +
iename +
"' which was called from a WHERE clause. This statement cannot be executed in " +
"this context";
nestedQueryError = true;
ErrorManager.recordOrThrowError(7254, msg, false);
}
/**
* Indicate whether a nested query error has been detected and has not yet been cleared.
*
* @return {@code true} if a nested query error has been detected, else {@code false}.
*/
boolean isNestedQueryError()
{
return nestedQueryError;
}
/**
* Changes the unknown mode.
*
* @param unknownMode
* The new unknown mode.
*
* @return A {@code Runnable} which should be called in a {@code finally} block, paired with the call to
* this method, which restores the unknown mode to its previous value.
*/
boolean setUnknownMode(boolean unknownMode)
{
boolean previous = this.unknownMode;
this.unknownMode = unknownMode;
return previous;
}
/**
* Checks whether the unknown mode is active.
*
* @return the current state of unknown mode flag.
*/
boolean isUnknownMode()
{
return unknownMode;
}
/**
* Add potentially unused keys to be reclaimed after the end of the current transaction or
* as buffer scopes are closed after a transaction. Some of these keys may not be collected,
* if the locks on them are downgraded instead of released.
*
* @param persistence
* Persistence helper object for a particular database.
* @param keys
* Keys to be reclaimed.
*/
void addPendingReclaimedKeys(Persistence persistence, List<Long> keys)
{
if (keys.isEmpty())
{
return;
}
Set<Long> pending = pendingReclaimedKeys.get(persistence);
if (pending == null)
{
pending = new HashSet<>();
pendingReclaimedKeys.put(persistence, pending);
}
pending.addAll(keys);
}
/**
* Reclaim unused keys collected during the transaction which just ended or from the buffer
* scope which just closed.
*
* @param persistence
* Persistence helper object for a particular database.
* @param released
* Keys actually released, due to the corresponding locks on them being released
* (as opposed to downgraded).
*/
void reclaimPendingKeys(Persistence persistence, Set<Long> released)
{
if (released == null)
{
return;
}
Set<Long> pending = pendingReclaimedKeys.get(persistence);
if (pending == null)
{
return;
}
IdentityManager identityManager = persistence.getIdentityManager(true);
if (identityManager == null)
{
return;
}
// only reclaim keys actually released (not downgraded); downgraded keys are still
// pinned by open buffers and cannot be reclaimed in their locked state
released.retainAll(pending);
int size = released.size();
if (size == 0)
{
return;
}
try
{
identityManager.reclaimKeys(released.toArray(new Long[size]));
}
finally
{
if (pending.removeAll(released) && pending.isEmpty())
{
pendingReclaimedKeys.remove(persistence);
}
}
}
/**
* Called by {@link RecordBuffer} when a new buffer scope is opened. It registers the open scope with the
* buffer manager, so that the buffer can receive block scope start and stop notifications, and be
* registered for commit/rollback/validate processing when a full transaction is begun.
*
* @param buffer
* Buffer for which a new scope is being opened.
*/
void openScope(RecordBuffer buffer)
{
boolean inTx = isTransaction();
if (buffer.isActive())
{
openBuffers.add(false, buffer);
buffer.addOpenBuffersScope(openBuffers.scopes());
if (inTx)
{
maybeActivateTxWrapper(buffer.getDatabase());
}
trackDatabase(buffer, true);
}
else
{
// a dynamic buffer is added to the global scope; also provisionally mark a buffer associated with
// a persistent procedure as needing to be added to the global scope; this may be overridden later
// by bufferInitialized
Boolean global = buffer.isDynamic() || buffer.getPersistentProc() != null
? Boolean.TRUE
: Boolean.FALSE;
uninitializedOpenBuffers.put(buffer, global);
}
}
/**
* Register static temp-table when its scope is opened.
*
* @param table
* The static temp-table.
* @param global
* Indicates if it is a global temp-table (declared as SHARED GLOBAL).
*/
void registerStaticTempTable(StaticTempTable table, boolean global)
{
Class<?> def = ((BufferImpl) table.defaultBufferHandleNative()).buffer().getDefiningType();
StaticDefinitionKey key = new StaticDefinitionKey(table._name(), pm._thisProcedure(), def);
openStaticTempTables.addEntry(global, key, table);
}
/**
* Register static temp table, when the external program gets executed.
*
* @param table
* The static temp-table.
* @param global
* Indicates if it is a global temp table (declared as SHARED GLOBAL).
*/
void registerPendingStaticTempTable(StaticTempTable table, boolean global)
{
registerPendingScopeable(this);
pendingStaticTempTables.put(table, global);
}
/**
* Check if the specified handle instance is registered as an OUTPUT argument.
*
* @param h
* The handle instance.
*
* @return <code>true</code> if the handle originates from the caller as an OUTPUT argument.
*/
public boolean isOutputTableHandle(handle h)
{
for (int i = 0; i < thOutputParameters.size(); i++)
{
Map<OutputTableHandleCopier, Integer> map = thOutputParameters.getDictionaryAtScope(i, true);
for (OutputTableHandleCopier out : map.keySet())
{
if (out.getCalled() == h)
{
return true;
}
}
}
return false;
}
/**
* Register the output table copier.
*
* @param copier
* Copier to register.
*/
void registerOutputTableHandleCopier(OutputTableHandleCopier copier)
{
Map<OutputTableHandleCopier, Integer> map = thOutputParameters.getDictionaryAtScope(0, true);
thOutputParameters.addEntry(false, copier, map.size());
copier.setSiblingsMap(map);
}
/**
* Unregister the output table copier <b>in the current scope</b>.
*
* @param copier
* Copier to unregister.
*/
void unregisterOutputTableHandleCopier(OutputTableHandleCopier copier)
{
thOutputParameters.removeEntryAtScope(copier, 0);
}
/**
* Get definition for the active static temp table with the given name.
*
* @param defType
* The type where this call is being made from.
* @param name
* Legacy name of the target static temp table.
*
* @return Definition of the static temp table with the given name or <code>null</code> if
* there is no active table with the given name.
*/
StaticTempTable getStaticTempTable(Class<?> defType, String name)
{
if (defType == null)
{
defType = pm.getExecuting();
}
if (defType == null)
{
throw new IllegalStateException("Null executing type when looking for " + name + " table");
}
if (!pendingStaticTempTables.isEmpty())
{
for (StaticTempTable table : pendingStaticTempTables.keySet())
{
Class<?> tdef = pendingBufferClasses.get(table.defaultBuffer());
if (tdef == defType && table._name().equalsIgnoreCase(name))
{
return table;
}
}
}
Class<?> type = defType;
return (StaticTempTable) pm.searchInStack((ref) -> getStaticTempTable(type, name, ref));
}
/**
* Get definition for the active static dataset with the given name.
* <p>
* The lookup is done first from super-classes and after that in {@link #dataSetRegistry}.
*
* @param defType
* The type where this call is being made from.
* @param name
* Legacy name of the target static dataset.
* @param referent
* The reference used to lookup this dataset by name.
*
* @return The found dataset with the same legacy name.
*/
DataSet getStaticDataSet(Class<?> defType, String name, Object referent)
{
StaticDefinitionKey key = new StaticDefinitionKey(name, referent, defType);
Object thisProc = pm._thisProcedure();
PersistentProcScope ppScope = persistProcScopes.get(thisProc);
DataSet ds = null;
if (ppScope != null)
{
ds = ppScope.datasets.get(key);
if (ds == null)
{
// check the defType hierarchy, in the same referent
if (BaseObject.class.isAssignableFrom(defType))
{
Class<?> type = defType.getSuperclass();
while (type != BaseObject.class)
{
StaticDefinitionKey key2 = new StaticDefinitionKey(name, referent, type);
ds = ppScope.datasets.get(key2);
if (ds != null)
{
break;
}
type = type.getSuperclass();
}
}
}
}
return ds != null ? ds : dataSetRegistry.lookup(key);
}
/**
* Get definition for the active static temp table with the given name.
*
* @param defType
* The type where this call is being made from.
* @param name
* Legacy name of the target static temp table.
* @param referent
* The external program where it was defined.
*
* @return definition of the static temp table with the given name or <code>null</code> if
* there is no active table with the given name.
*/
StaticTempTable getStaticTempTable(Class<?> defType, String name, Object referent)
{
StaticDefinitionKey key = new StaticDefinitionKey(name, referent, defType);
return openStaticTempTables.lookup(key);
}
/**
* Get the depth of the scope at which the static temp table was opened.
*
* @param name
* Legacy name of the target static temp table.
*
* @return zero-based index of the target scope, starting from the outermost scope. If there is
* no table with the given name, <code>-1</code> is returned.
*/
int getStaticTempTableDepth(String name)
{
Integer val = (Integer) pm.searchInStack((ref) -> getStaticTempTableDepth(name, ref));
return val == null ? -1 : val;
}
/**
* Get the depth of the scope at which the static temp table was opened.
*
* @param stt
* The static temp-table instance.
*
* @return zero-based index of the target scope, starting from the outermost scope. If there is
* no table with the given name, <code>-1</code> is returned.
*/
int getStaticTempTableDepth(StaticTempTable stt)
{
Map<StaticDefinitionKey, StaticTempTable> scope = null;
for (int i = 0; i < openStaticTempTables.size(); i++)
{
scope = openStaticTempTables.getDictionaryAtScope(i, false);
if (scope.values().contains(stt))
{
return openStaticTempTables.size() - i - 1;
}
}
return -1;
}
/**
* Get the depth of the scope at which the static temp table was opened.
*
* @param name
* Legacy name of the target static temp table.
* @param referent
* The external program where it was defined.
*
* @return zero-based index of the target scope, starting from the outermost scope. If there is
* no table with the given name, <code>-1</code> is returned.
*/
int getStaticTempTableDepth(String name, Object referent)
{
Class<?> def = pm.getExecuting();
StaticDefinitionKey key = new StaticDefinitionKey(name, referent, def);
int topDepth = openStaticTempTables.locate(key);
return topDepth == -1 ? -1 : openStaticTempTables.size() - topDepth - 1;
}
/**
* Add the target buffer to the set of all buffers at the specified block depth.
*
* @param blockDepth
* Zero-based depth of the block (starting from the outermost block) at which the
* buffer was opened.
* @param buffer
* Buffer to add.
*/
void addToAllBuffersArray(int blockDepth, RecordBuffer buffer)
{
Object referent = pm._thisProcedure();
registerToBuffersByClass(blockDepth, buffer);
allBuffers.addEntryAt(blockDepth, buffer.getDMOToken(), buffer);
buffer.addAllBufferScope(allBuffers.size() - blockDepth);
buffer.addByLegacyNameScope(byLegacyName.size() - blockDepth);
Map<Object, Map<String, RecordBuffer>> refs = byLegacyName.getDictionaryAtScope(blockDepth, true);
Map<String, RecordBuffer> bufDefs = refs.get(referent);
if (bufDefs == null)
{
refs.put(referent, bufDefs = new CaseInsensitiveHashMap<>());
}
bufDefs.put(buffer.getLegacyName(), buffer);
}
/**
* Called by {@link TemporaryBuffer} when a new dynamic buffer scope is opened to register the
* open scope with the buffer manager, so that the buffer can receive block scope start and
* stop notifications, and be registered for commit processing when a full transaction is
* begun.
*
* @param openScopeDepth
* Zero-based depth of the open scopes (starting from the outermost scope) at which this
* buffer scope is opened.
* @param buffer
* Buffer for which a new scope is being opened.
*/
void openScopeAt(int openScopeDepth, RecordBuffer buffer)
{
// TODO: this seems to be called only for 'openScopeDepth == 0'.
if (buffer.isActive())
{
if (openScopeDepth == 0)
{
openBuffers.add(true, buffer);
buffer.addOpenBuffersScope(1); // global scope
}
else
{
openBuffers.addAt(getOpenBufferScopes() - openScopeDepth, buffer);
buffer.addOpenBuffersScope(openScopeDepth);
}
if (isTransaction())
{
maybeActivateTxWrapper(buffer.getDatabase());
}
trackDatabase(buffer, true);
}
else
{
uninitializedOpenBuffers.put(buffer, openScopeDepth == 0 ? Boolean.TRUE : Boolean.FALSE);
}
}
/**
* A buffer was initialized. If it was previously stored as an uninitialized, open buffer, move it into
* the active, open buffers list. Perform special handling for persistent procedure buffers.
*
* @param buffer
* Record buffer which was initialized.
*
* @return {@code true} if this was properly registered as an open buffer. {@code false} if this couldn't
* be properly registered because the scope wasn't opened properly beforehand.
*/
boolean bufferInitialized(RecordBuffer buffer)
{
Boolean global = uninitializedOpenBuffers.remove(buffer);
if (global == null)
{
// buffer was not found among the pending, uninitialized buffers; nothing to do
return buffer.getOpenScopeCount() > 0;
}
// dynamic buffers always are added to the global scope; persistent procedure buffers need further
// inspection to determine whether they are globally scoped or not...
if (global && !buffer.isDynamic())
{
Object referent = buffer.getPersistentProc();
if (referent != null)
{
// a buffer associated with a persistent procedure will have been provisionally marked global,
// but this is overridden if the persistent procedure is still on the stack (in which case it
// is added to the scope corresponding with the buffer's activeScopeDepth)
global = pm.isExecuted(referent);
if (global)
{
// the procedure is no longer on the stack, so we must explicitly add the buffer to the
// PersistentProcScope.openBuffers collection for the PersistentProcScope which corresponds
// with the procedure
PersistentProcScope ppScope = persistProcScopes.computeIfAbsent(referent, k ->
{
return new PersistentProcScope();
});
ppScope.openBuffers.add(buffer);
}
}
}
if (global)
{
openBuffers.add(true, buffer);
buffer.addOpenBuffersScope(1); // global scope
}
else
{
int scopes = getOpenBufferScopes();
if (buffer.activeScopeDepth <= scopes)
{
openBuffers.addAt(scopes - buffer.activeScopeDepth, buffer);
buffer.addOpenBuffersScope(buffer.activeScopeDepth);
}
}
trackDatabase(buffer, true);
return true;
}
/**
* Given a database which has one or more buffers with an open scope operating within an application
* level transaction, possibly activate the transaction wrapper associated with that database.
* Activation entails starting a database-level transaction and activating a savepoint manager
* for that transaction.
* <p>
* If the transaction wrapper for the given database already is active, do nothing.
*
* @param database
* Database with at least one buffer with an open scope.
*/
void maybeActivateTxWrapper(Database database)
{
txWrapperHelper.maybeActivateTxWrapper(database);
}
/**
* Find the record buffer associated with the given DMO alias.
*
* @param alias
* Buffer DMO alias in business logic. (AKA Java name)
*
* @return Associated record buffer or {@code null} if not found.
*/
RecordBuffer lookupBuffer(String alias)
{
for (RecordBuffer buffer : pendingBuffers)
{
String token = buffer.getDMOToken();
if (token != null && token.equals(alias))
{
return buffer;
}
}
return allBuffers.lookup(alias);
}
/**
* Find the record buffer associated with the given legacy name, which is the variable name
* with which the buffer was defined. Because this method is legally called only after the
* procedure block started, the lookup is not performed on a structure similar to
* {@link #pendingBuffers}.
*
* @param legacyName
* Buffer variable's legacy name in business logic.
*
* @return Associated record buffer or {@code null} if not found.
*/
RecordBuffer lookupByName(String legacyName)
{
// look in the current stack of executing Class
Iterator<Object> it = pm.destinationIterator();
while (it.hasNext())
{
Object ref = it.next();
Map<String, RecordBuffer> bufDefs =
byLegacyName.lookup(ref, byLegacyName.size(), (Map<String, RecordBuffer> defs) ->
{
return defs != null && defs.containsKey(legacyName);
});
if (bufDefs != null && bufDefs.containsKey(legacyName))
{
return bufDefs.get(legacyName);
}
// lookup in the persistent procedure, too
PersistentProcScope ppScope = persistProcScopes.get(ref);
if (ppScope != null)
{
bufDefs = ppScope.byLegacyName.get(ref);
if (bufDefs != null && bufDefs.containsKey(legacyName))
{
return bufDefs.get(legacyName);
}
}
}
return null;
}
/**
* Get an iterator on all open buffers for the given persistence object, within the current client context.
*
* @param p
* A {@code Persistence} object which manages sessions. Only record buffers associated with this
* persistence object are returned.
* @param multiTenantOnly
* If {@code true}, only buffers of multi-tenant tables are filtered to be returned.
* Otherwise, all opened buffers are returned in the iterator.
*
* @return Iterator on all (initialized) buffers.
*/
Iterator<RecordBuffer> getBuffersOf(Persistence p, boolean multiTenantOnly)
{
Set<RecordBuffer> ret = new HashSet<>();
for (RecordBuffer next : openBuffers)
{
if (next.isActive() && p.equals(next.getPersistence()))
{
if (!multiTenantOnly || next.getDmoInfo().multiTenant)
{
ret.add(next);
}
}
}
return ret.iterator();
}
/**
* Get an iterator on all open buffers which contain non-<code>null</code>
* current records for the given persistence object, within the current
* client context. Auto-commit record buffers associated with a pending,
* reversible rollback are not included in the iteration. Records that
* represent copies of DMOs from other sessions, obtained from a {@link
* DirtyShareContext}, are likewise excluded.
*
* @param persistence
* Persistence object which manages sessions. Only record
* buffers associated with this persistence object are returned.
*
* @return Iterator on all active buffers.
*/
Iterator<RecordBuffer> activeBuffers(Persistence persistence)
{
return activeBuffers(persistence, false);
}
/**
* Get an iterator on all open buffers which contain non-<code>null</code>
* current records for the given persistence object, within the current
* client context. Auto-commit record buffers associated with a pending,
* reversible rollback are not included in the iteration. Records that
* represent copies of DMOs from other sessions, obtained from a {@link
* DirtyShareContext}, are likewise excluded.
*
* @param persistence
* Persistence object which manages sessions. Only record
* buffers associated with this persistence object are returned.
* @param includeTransient
* <code>true</code> to include transient records in the result set.
*
* @return Iterator on all active buffers.
*/
Iterator<RecordBuffer> activeBuffers(Persistence persistence, boolean includeTransient)
{
Set<RecordBuffer> set = new HashSet<>();
for (RecordBuffer next : openBuffers)
{
if (isActiveBuffer(next, persistence, includeTransient))
{
set.add(next);
}
}
return set.iterator();
}
/**
* Check if the given buffer contains a non-<code>null</code> current record for
* the given persistence object, within the current client context. Auto-commit
* record buffers associated with a pending, reversible rollback are not included
* in the iteration. Records that represent copies of DMOs from other sessions,
* obtained from a {@link DirtyShareContext}, are likewise excluded.
*
* @param buffer
* RecordBuffer that is checked.
* @param persistence
* Persistence object which manages sessions. Only record
* buffers associated with this persistence object are returned.
* @param includeTransient
* <code>true</code> to include transient records in the result set.
*
* @return <code>true</code> if the buffer has a non-<code>null</code> current
* record, <code>false</code> otherwise.
*/
static boolean isActiveBuffer(RecordBuffer buffer, Persistence persistence, boolean includeTransient)
{
if (buffer.isActive() &&
(includeTransient || !buffer.isTransient()) &&
!buffer.isDirtyCopy() &&
!buffer.isFake() &&
persistence.equals(buffer.getPersistence()) &&
buffer.getCurrentRecord() != null)
{
return true;
}
return false;
}
/**
* Evict the given DMO from the current ORM session if it is not referenced by any buffer or being
* tracked for UNDO purposes. This method does not modify the DMO's in-use status. It is intended to
* quickly evict DMOs which have been loaded into the session by side effect.
*
* @param persistence
* Persistence object which manages sessions.
* @param local
* Current user persistence context.
* @param dmo
* DMO instance whose reference count is to be decremented.
*
* @return {@code true} if the DMO was evicted; {@code false} if it was in use.
*
* @throws PersistenceException
* if an error occurs detaching {@code dmo} from the persistence session.
*/
boolean evictDMOIfUnused(Persistence persistence, Persistence.Context local, Record dmo)
throws PersistenceException
{
if (!dmo.isInUse())
{
if (local == null)
{
local = persistence.getContext(!dmo._getRecordMeta().getDmoMeta().multiTenant);
}
persistence.evict(local, dmo);
return true;
}
return false;
}
/**
* Notify the buffer manager that a new record was loaded (or the old record was reloaded)
* into the specified record buffer.
*
* @param recordBuffer
* The record buffer in which a record was loaded (or the old record was reloaded).
*/
void notifyRecordWasLoaded(RecordBuffer recordBuffer)
{
txWrapperHelper.notifyRecordWasLoaded(recordBuffer);
}
/**
* Generate name (DMO alias) for a dynamic buffer. The name is
* generated by using standard P2J buffer name conversion rules.
*
* @param legacyBufferName
* Legacy name of the buffer.
*
* @return name (alias) for a dynamic buffer.
*/
String generateBufferName(String legacyBufferName)
{
String javaName = convertedNames.get(legacyBufferName);
if (javaName == null)
{
javaName = nc.convert(legacyBufferName, MatchPhraseConstants.TYPE_DMO_ALIAS);
convertedNames.put(legacyBufferName, javaName);
}
return javaName;
}
/**
* Generate unique (across the session) name (DMO token) for a dynamic buffer. The name is
* generated by using standard P2J buffer name conversion rules and if necessary, appending
* <code>__{unique number}</code> postfix. This guarantees that the name will not contain
* invalid characters and will be unique.
*
* @param legacyBufferName
* Legacy name of the buffer.
*
* @return unique name (alias) for a dynamic buffer.
*/
String generateUniqueToken(String legacyBufferName)
{
String javaName = generateBufferName(legacyBufferName);
StringBuilder buf = new StringBuilder();
buf.append(javaName);
buf.append("__");
buf.append(tokenCounter++);
return buf.toString();
}
/**
* Iterate all active persistence contexts and close any open session, if there are no longer
* any dependencies on it. This method deregisters an active persistence context by side
* effect, if its open session was closed.
*/
private void maybeCloseSessions()
{
if (activePersistenceContexts.isEmpty())
{
return;
}
Iterator<Persistence.Context> iter = activePersistenceContexts.iterator();
while (iter.hasNext())
{
Persistence.Context pc = iter.next();
if (pc.canCloseSession())
{
try
{
if (pc.closeSession(false))
{
// remove the active persistence context if the close attempt succeeded
iter.remove();
}
}
catch (PersistenceException exc)
{
if (LOG.isLoggable(Level.SEVERE))
{
String msg = "Error closing database session (" +
pc.getPersistence().getDatabase(Persistence.PRIVATE_CTX) +
")";
LOG.log(Level.SEVERE, msg, exc);
}
}
}
}
}
/**
* Add the source buffers to the destination map.
*
* @param dst
* The destination map.
* @param src
* The source map.
*/
private void checkLegacyNames(Map<Object, Map<String, RecordBuffer>> dst,
Map<Object, Map<String, RecordBuffer>> src)
{
for (Map.Entry<Object, Map<String, RecordBuffer>> entry : src.entrySet())
{
Object ref = entry.getKey();
Map<String, RecordBuffer> srcBufs = entry.getValue();
Map<String, RecordBuffer> dstBufs = dst.computeIfAbsent(ref, (k) -> new CaseInsensitiveHashMap<>());
dstBufs.putAll(srcBufs);
}
}
/**
* Track the buffer's database. This will increment the {@link TxWrapper.WorkArea#activeDatabases} counter when a
* new buffer is activated, and decrement it when it exists its scope.
*
* @param buffer
* The buffer to track.
* @param add
* Flag indicating if the buffer is opening or closing the scope.
*
* @throws IllegalStateException
* If we are removing an active buffer and there is no entry for it in
* {@link TxWrapper.WorkArea#activeDatabases}.
*/
private void trackDatabase(RecordBuffer buffer, boolean add)
{
txWrapperHelper.trackDatabase(buffer, add);
}
/**
* Register the provided buffer to a provided block depth of {@link #buffersByDMOBufClass}.
*
* @param blockDepth
* The block depth at which the buffer should be registered.
* @param buffer
* The buffer to be registered.
*/
private void registerToBuffersByClass(Integer blockDepth, RecordBuffer buffer)
{
String dmoBufIface = buffer.getDMOBufInterface().getName();
SimpleEntry<String, Integer> key = new SimpleEntry<>(dmoBufIface, buffer.getMultiplexID());
Set<RecordBuffer> recordBuffersPerTable;
if (blockDepth != null)
{
// add to specified scope
recordBuffersPerTable = buffersByDMOBufClass.getValueAtScope(key, blockDepth);
if (recordBuffersPerTable == null)
{
recordBuffersPerTable = new HashSet<>();
buffersByDMOBufClass.addEntryAt(blockDepth, key, recordBuffersPerTable);
}
// this is required so that dynamic buffers have a change to do their own clear-up when deleted
buffer.addBufferByClassScope(buffersByDMOBufClass.size() - blockDepth);
}
else
{
// add to last scope
recordBuffersPerTable = buffersByDMOBufClass.getValueAtScope(key, 0);
if (recordBuffersPerTable == null)
{
recordBuffersPerTable = new HashSet<>();
buffersByDMOBufClass.addEntry(false, key, recordBuffersPerTable);
// this is required so that dynamic buffers have a change to do their own clear-up when deleted
buffer.addBufferByClassScope(buffersByDMOBufClass.size());
}
}
recordBuffersPerTable.add(buffer);
}
/**
* Indicate whether the current block is "important" from the standpoint of transitioning into
* or out of its scope. A block is considered "important" in this sense if it is either
* completely outside of an application transaction, or if it is within a transaction AND has transaction
* block properties (i.e., is not a NO_TRANSACTION block).
* <p>
* This notion of importance is used to determine whether the various scoped data structures managed by
* this class need to add or delete a scope when transitioning into or out of the block, respectively,
* and whether we need to perform processing (e.g., copying/moving data between scopes) associated with
* such transitions.
*
* @return {@code true} if the block is considered "important" by the above criteria, else
* {@code false}.
*/
public boolean isImportantBlockTransition()
{
return txWrapperHelper.isImportantBlockTransition();
}
/**
* Container for batch assign mode information.
*/
private static class BatchModeData
{
/** Marks a not active scope. */
public static final int NOT_ACTIVE = -1;
/**
* Marks an internal scope. Internal scopes are scopes artificially introduced for a FWD specific
* data management. This must always be the last scope and must be popped out before other normal (4GL-)
* requested batch scopes are inserted.
*/
public static final int INTERNAL = -2;
/** Scope depth at which batch assign mode began; -1 if not in batch assign mode */
private int batchDepth = NOT_ACTIVE;
/** Map of dirty batch assign mode buffers with corresponding code that restores their active state.*/
private Map<RecordBuffer, Runnable> dirtyBatchBuffers = null;
/**
* Default constructor.
*/
BatchModeData()
{
}
/**
* Activate batch assign mode.
*
* @param batchDepth
* Scope depth at which batch assign mode was activated.
*/
void activate(int batchDepth)
{
this.batchDepth = batchDepth;
dirtyBatchBuffers = new LinkedHashMap<>();
}
/**
* Obtain a string encoding of the batch mode data.
*/
@Override
public String toString()
{
return "BMData{" +
(batchDepth == INTERNAL ? "INTERNAL" : batchDepth == NOT_ACTIVE ? "NOT-ACTIVE" : batchDepth) +
", buffers: #" + (dirtyBatchBuffers == null ? 0 : dirtyBatchBuffers.size()) + '}';
}
}
/**
* Buffer-related data for a certain persistent procedure, surviving after its external
* procedure is finished.
*/
private class PersistentProcScope
{
/**
* All defined record buffers for a persistent procedure. This data structure uses
* the DMO alias as keys.
* TODO: this can't be by String
*/
private final Map<String, RecordBuffer> allBuffers = new HashMap<>();
/**
* Scoped dictionary in which all defined record buffers are stored. This data structure uses
* the <strong>lowercased legacy name</strong> as keys.
* <p>
* Note: only the static buffers are kept in this structure. The dynamic resources cannot be
* stored here as their NAME attribute is mutable and will possible collide with other
* existing buffers.
*/
private final Map<Object, Map<String, RecordBuffer>> byLegacyName = new IdentityHashMap<>();
/**
* Scoped dictionary with key as a DMO buffer interface and multiplex, and the value is a list of
* RecordBuffers associated with them.
*/
private final Map<SimpleEntry<String, Integer>, Set<RecordBuffer>> buffersByDMOBufClass =
new HashMap<>();
/** Open record buffers for a persistent procedure. */
private final Set<RecordBuffer> openBuffers = new LinkedHashSet<>();
/**
* Active static temp tables (i.e. table scope was opened and not yet closed) for a
* persistent procedure.
*/
private final Map<StaticDefinitionKey, StaticTempTable> openStaticTempTables = new HashMap<>();
/** The static datasets defined in this instance. */
private final Map<StaticDefinitionKey, DataSet> datasets = new HashMap<>();
}
/**
* A temp-table key contains the lowercase legacy name of the dataset/table and the procedure where it
* was created. This way we can be sure there are no collisions between datasets/temp tables having the
* same name but created by different procedures.
*/
private class StaticDefinitionKey
{
/** The lowercased temp-table legacy name. */
private final String legacyName;
/** The parent procedure where the temp-table was created. */
private final Object parentProcedure;
/** The Java class defining this table. */
private final Class<?> def;
/**
* The hash value is computed when the key is created. These keys are immutable so the hash
* is computed only once and cached.
*/
private final int hash;
/**
* The contructors copies the parameters to internal data and computed the hash value.
*
* @param legacyName
* The lowercased temp-table legacy name.
* @param parentProcedure
* The parent procedure where the temp-table was created.
* @param def
* The Java class defining this buffer.
*/
public StaticDefinitionKey(String legacyName, Object parentProcedure, Class<?> def)
{
// do not lowercase, 'equals' uses 'equalsIgnoreCase' and the hash is case-insensitive
this.legacyName = legacyName;
this.parentProcedure = parentProcedure;
this.def = def;
int lhash = StringHelper.hashCodeCaseInsensitive(legacyName);
hash = (lhash * 31 + parentProcedure.hashCode()) * 31 + def.hashCode();
}
/**
* Checks whether the alien object is equals to this one.
* @param o
* The alien object.
*
* @return {@code true} only if the two objects are the same (same temp-table created
* within the same procedure).
*/
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
StaticDefinitionKey that = (StaticDefinitionKey) o;
if (this.hash != that.hash)
{
return false;
}
if (!legacyName.equalsIgnoreCase(that.legacyName) || def != that.def)
{
return false;
}
return parentProcedure.equals(that.parentProcedure);
}
/**
* Obtain the hash code as it was computed in the constructor.
*
* @return the hash value for this key.
*/
@Override
public int hashCode()
{
return hash;
}
}
}