RecordBuffer.java
/*
** Module : RecordBuffer.java
** Abstract : Encapsulates a data record and provides methods to operate on it
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050816 @22479 Created initial version. Abstract base class
** of all DMO classes which provides integration
** with stateful, navigable queries.
** 002 ECF 20050916 @22934 Complete rewrite to use dynamic proxies. The
** buffer is now a container for a DMO reference
** which may change during the course of query
** execution. The buffer reference is registered
** with the transaction manager, possibly more
** than once, by client code.
** 003 ECF 20051028 @23244 First cut at full transaction support. This
** includes undo processing, transaction
** commit/rollback, automatic lock upgrade,
** downgrade, release, and record release. Added
** logging as well.
** 004 ECF 20051106 @23375 Added real-time update validation. Introduced
** use of DMOValidator class. Also made various
** improvements to transaction support and
** adjustments to accommodate use with compound
** and preselect queries.
** 005 ECF 20051129 @23566 Added support for batch assign mode. DMO
** updates must be bracketed by static method
** calls to startBatch and endBatch.
** 006 ECF 20051205 @23669 Integrated with ConnectionManager. Uses CM
** services at construction to determine logical
** and physical database names. Notifies CM of
** buffer scope open/close events.
** 007 ECF 20051216 @23748 Implemented various Progress language
** statement and built-in function replacements:
** copy, compare, ambiguous, new, and locked.
** 008 ECF 20060104 @23830 Added support for temporary records. These
** are used for client-side (i.e., non-database)
** record sorting.
** 009 ECF 20060117 @23951 Honor pending error status when invoking DMO
** setter methods. If in silent error mode and
** and error is pending, assignment of DMO
** properties is not permitted.
** 010 ECF 20060120 @24042 Replaced ThreadLocal with ContextLocal. Minor
** changes to logging.
** 011 ECF 20060201 @24350 Added temp table support. This required
** removing the final modifier from the class,
** making numerous private methods protected,
** and abstracting/adding several new virtual
** methods to make this class extensible.
** 012 ECF 20060310 @25031 Added support for natural joins. Added new
** inner class ForeignSyncher to perform reverse
** foreign record resolution as legacy foreign
** key properties are changed by application
** code.
** 013 ECF 20060503 @25961 Fixed incorrect handling of latestRecord.
** This variable should not be updated when a
** record is newly created.
** 014 ECF 20060504 @25981 Improved error reporting. Better handling of
** DMO getter/setter index out of bounds errors.
** 015 ECF 20060504 @26007 Fixed defects in buffer copy. Indexed fields
** were not being managed properly, leading to
** exceptions.
** 016 GES 20060517 @26200 Match new Finalizable interface requirement.
** 017 ECF 20060522 @26588 Fixed delete processing. In addition to
** deleting the current record, it is removed
** from the buffer.
** 018 ECF 20060531 @26852 Replaced EndConditionException with
** QueryOffEndException. The latter is a more
** specific version of the former, used to
** indicate a query has run off the end of its
** result set.
** 019 ECF 20060608 @27025 Fixed defect in record create. After creating
** a DMO instance, all records joined to the DMO
** by legacy keys must be set to their defaults.
** 020 ECF 20060610 @27103 Fixed defect with wasLocked() reporting.
** Underlying locked flag was being cleared too
** aggressively.
** 021 ECF 20060621 @27507 Flush newly created records to database more
** aggressively. These are now flushed upon any
** validation of a new record to ensure that a
** query made subsequent to a property being set
** on a newly created record finds the new
** record.
** 022 ECF 20060627 @27592 Refined validation and flushing. We now track
** dirty properties and only validate and flush
** if changes to the DMO's state actually have
** been made. As a result, validation is more
** targeted.
** 023 ECF 20060628 @27669 Track nested open scopes. Added method to
** report a reference count of open scopes:
** getOpenScopeCount().
** 024 ECF 20060630 @27790 More fixes to flushing/validating. Separated
** flushing from validation logic. Added a
** rollback capability for invalid buffers.
** 025 ECF 20060720 @28119 Major enhancements. Implemented the change
** broker architecture and a more aggressive
** eviction policy for DMOs. The former enables
** numerous performance optimizations. The
** latter reduces the working set and improves
** the execution time of most session flushes,
** though eviction itself is somewhat costly.
** 026 ECF 20060728 @28273 Moved DMO reference counting mechanism to
** BufferManager. incrementDMOUseCount() and
** decrementDMOUseCount() are now implemented
** there.
** 027 ECF 20060831 @29088 Changed implementation of getSchema().
** 028 ECF 20060906 @29302 Fixed validation defect. Validation and
** flushing of a newly created record must not
** be triggered by a proxy setter method call
** until all columns of a unique constraint have
** been updated. The first unique constraint to
** have all its columns updated triggers the
** flush.
** 029 ECF 20060911 @29462 Fixed memory leak. Setting the same current
** record more than once was incrementing the
** DMO use count with the BufferManager too
** often.
** 030 ECF 20060916 @29667 Major changes to undo architecture. We no
** longer rely on the TransactionManager undo
** processing for record changes. Rather, we
** track property changes and record-level
** actions as they occur, saving instructions on
** how to rollback changes at each block scope.
** Also made some minor fixes to lock handling
** to prevent locks from being released too
** early in some cases.
** 031 ECF 20060918 @29676 Fix to setCurrentRecord(). Don't flush brand
** new records; instead, give business logic a
** chance to modify them first.
** 032 ECF 20060919 @29698 Introduced in/decrementDMOUseCount() methods
** which delegate to worker methods in
** BufferManager. Previously, the BufferManager
** methods were called directly, but the new
** methods allow subclasses to override the
** implementations. Also made processChange()
** protected, so that subclasses could override
** its implementation.
** 033 ECF 20060926 @29959 Back out #031 (@29676). This was an improper
** fix, in that we are not flushing the newly
** set record, but rather the record it is
** replacing. The latter record MUST be flushed,
** or it is possible to lose new records.
** 034 ECF 20061002 @30072 Fixed invocation handler. A getter method
** call on an empty buffer must return unknown
** value when the error manager is in silent
** error mode. Previously it returned null.
** 035 ECF 20061006 @30198 Fixed snapshot logic. A snapshot is never
** taken for newly created records, only for
** records read from the database. Modified
** implementation of isNew() to retain the
** knowledge of a record's newly created status,
** even after it is made persistent.
** 036 ECF 20061010 @30311 Made various fixes and added foundational
** support for auto-commit temp-tables. Added
** safety code to prevent an error from being
** raised if batch assign mode is interrupted
** prematurely, then restarted at a later time.
** 037 ECF 20061012 @30379 Fixed iterate() method. Must release current
** record in preparation for next pass of loop.
** 038 ECF 20061018 @30485 Moved processing to clean up batch assign
** mode to BufferManager. This was interfering
** with end-of-scope processing in some cases.
** 039 ECF 20061018 @30531 Integrated SessionListener architecture.
** The invocation handler now receives
** sessionClosing notifications. This allows the
** handler to force the Persistence object to
** bind to a Hibernate session before attempting
** a method call on a DMO. This is required now
** because a DMO may be proxied by Hibernate,
** and we no longer pin sessions open
** artificially. Thus, it is necessary to force
** a session bind in some cases, to avoid a
** LazyInitializationException.
** 040 ECF 20061023 @30596 Fixed defect in invocation handler. Newly
** created DMOs were being validated too early,
** before all properties in a unique constraint
** had been updated.
** 041 ECF 20061027 @30756 Replaced Object with DataModelObject for DMO
** arguments to methods/constructors. Required
** for compile-time type safety.
** 042 ECF 20061103 @30947 Fixed reportChange(). Bind the persistence
** service to a backing session if necessary,
** before invoking DMO's deepCopy() method. This
** allows the DMO's internal associations to be
** lazily populated by Hibernate if this was not
** previously done.
** 043 ECF 20061105 @31095 Fixed delete(). This method was not properly
** handling inverse foreign key relations to the
** deleted record. These are now nulled out when
** a record is deleted. The ForeignNuller class
** is used to track down dependent records and
** break their foreign key references to the
** deleted record. Also fixed a defect whereby a
** deleted record was not being cleared properly
** from other buffers which held the record.
** 044 ECF 20061114 @31141 Removed ForeignSyncher inner class. This code
** was refactored and expanded to create the
** external AssociationSyncher class and its
** subclasses.
** 045 ECF 20061114 @31144 Disabled prohibitively expensive foreign
** association synchronization operations. This
** effectively removes all such synchronization
** except setting the foreign DMO in a local DMO
** when a legacy key property in that local DMO
** is changed.
** 046 ECF 20061114 @31160 Moved batch assign mode state variables to
** BufferManager. This change enables us to
** employ a more discriminating batch mode
** cleanup mechanism because the batch mode
** can be tied to a specific block scope.
** 047 ECF 20061115 @31188 Fixed off-end behavior in setCurrentRecord().
** Clear the snapshot record only if setting a
** null current record AND this would be
** considered an error condition by the calling
** query.
** 048 ECF 20061115 @31211 Fixed regression caused by #047 (@31188).
** Added parameter to setCurrentRecord() method
** to allow caller to determine under what
** circumstance to reset snapshot.
** 049 ECF 20061116 @31237 Fixed nested buffer scope handling. Upon the
** closure of a nested buffer scope, cleanup
** processing was releasing the current record
** unconditionally. This must happen only upon
** closure of the outermost buffer scope.
** 050 ECF 20061116 @31242 Fixed error saving new records. Needed to
** register the need for a pending session flush
** with the ChangeBroker for the saved DMO type.
** Otherwise, Hibernate complains about possible
** nonthreadsafe access to the session.
** 051 ECF 20061120 @31327 Optimization. Do not create reversibles for
** create and delete events if buffer is not
** undoable.
** 052 ECF 20061207 @31620 Report record insert as a state change to the
** ChangeBroker. Previously, an insert caused a
** pending session flush but did not trigger a
** state change event to be sent to ChangeBroker
** listeners.
** 053 ECF 20061212 @31668 Fixed undoable processing for newly created
** records. In cases where the record's creation
** and its persistence to the database occurred
** in different scopes, undo was not working
** properly.
** 054 ECF 20070130 @32040 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. Moved
** Reversible inner class into a top level
** class. Also fixed copy implementation to
** flush newly created destination record at the
** appropriate time.
** 055 ECF 20070202 @32071 Changed signature for copy() worker method.
** It is necessary for this method to accept
** either a proxy or backing DMO object as the
** destination record. The former supports the
** buffer-copy implementation; the latter is
** used internally by TemporaryBuffer.
** 056 ECF 20070212 @32160 Rollback and flush fixes. Force session flush
** when DMOs are rolled back in memory. Fixed
** NPE when flushing a newly created record to
** database in some circumstances. Fixed
** database error when a newly created record
** was improperly flushed upon a buffer scope
** ending after a rollback.
** 057 ECF 20070221 @32229 Track reference count for DMOs managed by
** no-undo temp-tables. These had been excluded
** from the aggressive eviction policy.
** 058 ECF 20070306 @32288 Fixed change notification. Changes to
** multiple indexes in an extent field made in
** batch mode were not being processed and
** reported properly. Only the last change was
** being remembered.
** 059 ECF 20070307 @32336 No longer throw 'error not on file' when a
** null current record is set. This is now the
** responsibility of the caller, since different
** errors can be raised for the same condition,
** depending upon the calling context.
** 060 ECF 20070309 @32367 Fixed ClassCastException. Regression
** introduced with #058 (@32288).
** 061 ECF 20070318 @32444 Fixed defect persisting unmodified, new DMO
** instances. An unmodified, newly created DMO
** must not be flushed to the database.
** 062 ECF 20070322 @32546 Reworked flushing and validation. A record is
** flushed at the earlier of: all columns in
** any index are updated, or another record (or
** null) is stored as the current buffer record,
** or a query is about to be executed, or the
** outermost open buffer scope ends.
** 063 ECF 20070323 @32563 More flushing cleanup. This version of the
** class implements an interim solution for
** dirty checks before flushing a transient
** record. When checking whether all columns of
** any index are dirty, we use all indexes for
** temp tables, but only unique indexes for
** permanent tables. This is because index info
** for temp tables is available via DMOIndex,
** which is not the case for permanent tables.
** Ultimately, we will want to use JDBC database
** metadata to query this index information for
** both types.
** 064 ECF 20070412 @32986 Rearchitected record locking implementation.
** The same lock manager is used, but record
** locks within a session are now coordinated by
** one RecordLockContext instance per database,
** per session. Removed the LockState inner
** class, as this logic is now managed by the
** RecordLockContext class for all buffers in a
** session (by database), rather than by each
** buffer independently.
** 065 ECF 20070415 @32994 Fixed record delete notification regression.
** Notification of a record delete was not
** being broadcast via the ChangeBroker. Refined
** rollback-based deletes as well. Added static
** helper method to compare DMOs by primary key.
** 066 ECF 20070416 @33029 Integrated user interrupt handling.
** 067 ECF 20070503 @33391 Added registration of new instance with the
** BufferManager.
** 068 ECF 20070509 @33461 Fixed validation processing during bulk copy.
** Fixed isIndexDirty() implementation to be
** more complete in its index checks.
** 069 ECF 20070515 @33672 Fixed DMO proxy invocation handler. When a
** setter method is invoked, the underlying
** property must be marked dirty regardless of
** whether the value of the property actually
** changed. This is necessary for the proper
** function of the isIndexDirty() method, so
** that newly created records are flushed to the
** database at the appropriate time.
** 070 ECF 20070517 @33676 More changes to fine tune flush timing for a
** newly created record. Records in tables with
** no indices are flushed immediately upon
** creation (effectively just temp tables, since
** permanent tables always have indices). When
** in batch assign mode, any setter invocation
** causes the buffer to be marked dirty, even if
** the invocation did not change the value.
** 071 ECF 20070523 @33728 Fixed defect related to deletions and saves.
** Deletion of a record was not properly
** cleaning up the validation context and the
** dirty read manager. Likewise, saves related
** to undo processing were not updating these
** mechanisms.
** 072 ECF 20070525 @33769 Fixed LightweightUndoable processing. Need to
** increment/decrement DMO use counts properly
** when TransactionManager creates Undoable
** backup sets and when scopes end. Also moved
** registration of LightweightUndoable from
** constructor to when first buffer scope is
** opened. These changes ensure that undo
** processing does not store a detached DMO into
** the buffer.
** 073 ECF 20070531 @33909 Corrected handling of bulk setter methods.
** DMOs with indexed properties have bulk setter
** methods which accept a scalar value or an
** array of values to set multiple indices of
** property in one call. The invocation handler
** had to be reworked to properly detect, report
** and process change in these cases.
** 074 ECF 20070601 @33917 Moved registration with ChangeBroker. Instead
** of registering upon construction, we now wait
** until the outermost scope is opened. This
** prevents a memory leak from registering at
** the wrong scope.
** 075 ECF 20070403 @34361 Retrofit for ForeignNuller change. Changed
** invocation of ForeignNuller.breakLinkages()
** to use simpler signature.
** 076 ECF 20070706 @34403 Added deregisteredSessionListener() to
** invocation handler. Required by change to
** SessionListener interface.
** 077 ECF 20070706 @34415 Minor optimizations. Cache ForeignNuller
** objects lazily instead of letting the static
** method ForeignNuller.breakLinkages() gather
** these instances repeatedly for each deleted
** record. Replaced StringBuffer with
** StringBuilder. Cache ChangeBroker instance.
** 078 ECF 20070709 @34423 Minor cleanup. Optimized commit() and
** rollback() slightly.
** 079 ECF 20070726 @34719 Refined release on iterate behavior. Allow
** release on iterate to be overridden. This is
** necessary for compound queries, so that the
** outer buffers in a client-side join do not
** lose their state while records are found for
** inner buffers.
** 080 ECF 20070815 @34853 Improved temporary record handling for nested
** client-side where expressions. Necessary to
** support client-side, converted CAN-FIND
** statements nested within a where clause.
** 081 ECF 20070821 @34902 Added prepare() and disableSilentError()
** methods. These additions enable errors during
** query substitution parameter processing to be
** suppressed using silent error mode, just like
** in Progress during where clause processing.
** 082 ECF 20070827 @34992 Added support for restricted bulk deletes.
** A new static delete() method variant accepts
** a where clause and query substitution parms
** and delegates the bulk delete service to the
** appropriate RecordBuffer instance. Currently,
** this service is not supported for permanent
** tables (future optimization), but the
** addition of this infrastructure allows
** subclasses to implement support for this
** service.
** 083 ECF 20070926 @35248 Fixed undo processing defect. Snapshot was
** incorrectly being nulled out in cases where
** a TM undo cycle tried to reset the current
** record to itself. We now backup the latest
** snapshot and restore it upon undo.
** 084 ECF 20070907 @35293 Fixed ConnectionManager notifications. Calls
** to open and close buffer scopes notified the
** connection manager unconditionally, but this
** is only appropriate for permanent tables.
** This functionality was abstracted into
** protected methods which can be overridden by
** subclasses. Replaced database name string
** database information object.
** 085 ECF 20071018 @35591 Integrated PersistenceFactory.
** 086 ECF 20071029 @35633 Changed in[de]crementDMOUseCount() signature.
** Matches change in BufferManager.
** 087 ECF 20071130 @36124 Fixed setRecord(). Removed resetSnapshot
** parameter. This method no longer clears the
** snapshot when a null current record is set.
** In this circumstance, the placeholder
** snapshot must not be lost. Integrated
** generics.
** 088 ECF 20071205 @36265 Track off-end status and sort index of last
** query updating the buffer. Added sortIndex
** and offEnd parameters to setRecord(); added
** getOffEnd() method. This information is used
** by a find query to ensure it starts its
** search at the proper placeholder record.
** 089 ECF 20071221 @36546 Refined rollback of auto-commit temp records.
** Do not try to re-delete a record already not
** present in the database.
** 090 SVL 20071231 @36627 Next primary key generation is carried out
** to the separate function.
** 091 ECF 20080129 @36935 Added debug/trace level logging. Log all
** create and flush events, as well as the
** details of any rollbacks.
** 092 ECF 20080310 @37483 Added getDialect() convenience method. Also
** integrated generics.
** 093 SVL 20080312 @37428 The inverse synchronization is enabled and
** performed for newly created DMOs.
** 094 SVL 20080418 @38038 Added support for the inverse sorting.
** 095 ECF 20080424 @38150 Integrated generics.
** 096 SVL 20080421 @38098 Association Synchers are disabled if foreign
** keys are disabled.
** 097 SVL 20080430 @38175 Reversible actions and updates are removed
** from the current scope into the commit()
** if we are at the master transaction level.
** 098 ECF 20080506 @38220 Fixed automatic lock upgrade logic. No
** upgrade is necessary if the datum being set
** in the DMO is the same as the existing value.
** 099 SVL 20080512 @38260 Added pinnedLockTypes map which maps primary
** keys of past and present currentRecords to
** their pinned LockTypes.
** 100 SVL 20080516 @38291 Memory consumption optimization.
** 101 SVL 20080526 @38397 pinnedLockTypes are cleared into finished().
** 102 CA 20080604 @38539 Fixed the rollback of nested CREATE/DELETE
** actions, when they are applied for the same
** DMO. Fixed the rollback of nested UPDATE
** actions for the same DMO - the changes must
** be reported to the ChangeBroker. Ref: #38537
** 103 ECF 20080506 @38612 Integrated DirtyShareContext. Used to share
** uncommitted index changes across user
** sessions.
** 104 ECF 20080606 @38643 Removed ValidationContext references.
** Obsoleted by DirtyShareContext.
** 105 ECF 20080609 @38653 Fixed several regressions. LightweightUnoable
** was improperly flushing a transient record
** during undo processing. Refactored dirty
** index update sharing to permit temp tables to
** bypass this processing.
** 106 ECF 20080610 @38691 Fixed rollback of DMO property updates. Dirty
** share manager was not being notified of
** changes being rolled back.
** 107 CA 20080612 @38713 Fixed FIND statement record retrieval and a
** dirty share issue:the snapshot must be set to
** null when current block is finished and also
** the snapshot must save the new record on
** create.
** 108 ECF 20080612 @38729 Fixed rollback() in ReversibleCreate and
** ReversibleDelete. Removed optimization to let
** database handle rollback on full transaction
** boundaries. Our internal state (and that of
** Hibernate) was not correct in this case.
** 109 CA 20080616 @38787 Fixed rollback() in ReversibleCreate:
** reportChange(...) must be called on a record
** insert rollback, so all the registered
** listeners for this buffer will be notified.
** 110 ECF 20080626 @38961 Fixed ReversibleCreate.rollback(). Current
** record must be nulled out unconditionally,
** whether or not the record was persisted. Also
** removed immediate rollback of DMO updates
** during failed validation. Instead, we rely on
** the normal, TM-driven rollback mechanism.
** 111 CA 20080605 @38568 Implemented rollback of bulk delete actions.
** Fixed rollback of reversible update actions.
** 112 ECF 20080628 @39070 More undo/rollback fixes. We no longer pin
** DMOs to the current Hibernate session when
** they are stored in LightweightUndoable.
** Added special handling for sharing dirty
** changes when newly created records have not
** yet been stored in the primary database.
** 113 CA 20080702 @39018 Fixed ReversibleUpdate.rollback() - the map
** unreportedChanges was not populated correct
** when there are changes for extent fields.
** 114 CA 20080716 @39124 Modifications due to DirtyShareContext API
** changes. Fixed rollback - if there are dirty
** CREATE or DELETE actions, all the indexes for
** the entity will be locked before the chain of
** reversibles is processed.
** 115 CA 20080718 @39170 Fix ValidationException in not-transacted
** blocks.
** 116 ECF 20080723 @39171 Fixed flushing behavior for temp-tables with
** no indexes. Updates to such tables must be
** flushed to the database immediately.
** 117 CA 20080725 @39211 Fix rollback for when the reversibles
** reference "stale" records.
** 118 ECF 20080806 @39328 Added rowid support. Reimplemented recordID()
** and added rowID().
** 119 CA 20080808 @39358 Implemented no-undo temp-table support: these
** tables require no rollback; on each CREATE,
** DELETE or UPDATE event, the BufferManager is
** added a Reversible containing the current
** record state. Fixed subtle flow in
** detectChange() related to diff processing.
** 120 ECF 20080815 @39557 Support global DMO change events.
** 121 CA 20080826 @39567 Added rollbackPending which is called by TM
** when UNDO targets a parent block.
** 122 CA 20080815 @39466 Support API change in DBUtils. Register with
** the BufferManager at the end of the c'tor.
** 123 SIY 20080904 @39719 Inferred generics in buffer creation methods.
** 124 SVL 20080901 @39653 Key reclamation is performed into commit()
** and rollback().
** 125 ECF 20080925 @39948 Refactored Reversible concrete classes.
** 126 GES 20080926 @39974 Minor signature change.
** 127 ECF 20081001 @39998 Changed openScope() to use varargs. This
** allows the conversion to compact multiple
** openScope() calls in business logic to a
** single call. Also fixed NPE in
** ReversibleDelete.getId(). Added versions of
** certain static helpers which return primitive
** boolean instead of logical.
** 128 ECF 20081007 @40049 Removed deprecated available() method.
** 129 ECF 20081020 @40209 Fixed rollback at full transaction. Cannot
** create/update/delete DMO at full transaction
** boundary just before database rollback.
** 130 ECF 20081031 @40286 Fixed delete of DMOs with extent fields. In
** some cases, DMOs with uninitialized, lazy,
** persistent collections were being stored in
** ReversibleDelete objects. Upon rollback, the
** missing collection data was unavailable.
** 131 ECF 20081106 @40326 Fixed errorNotOnFile(). Use DMO alias instead
** of DMO interface name.
** 132 ECF 20081114 @40453 Fixed record lock leak. Record locks must be
** relinquished in iterate() and retry() if we
** are at the outermost open scope.
** 133 ECF 20081118 @40550 Fixed ReversibleDelete. RecordBuffer's
** current record DMO must be used for rollback
** if it represents the same record as the
** ReversibleDelete's internal DMO. Removed
** processing in RecordBuffer.rollback() which
** was intended to handle this case, but was
** incorrect.
** 134 ECF 20081201 @40733 Added support for key reclamation for temp
** tables. Uses the same hooks as we had for key
** reclamation in permanent tables (commit() and
** rollback() methods). A new reclaimKeys(List)
** method allows subclasses to override the
** original implementation.
** 135 SVL 20081204 @40794 Added undoables map cleanup into rollback().
** 136 ECF 20081211 @40870 Better debug information in toString().
** 137 ECF 20081215 @40918 Modified transient record flushing. Removed
** flush(Method, Object[]) variant. Simplified
** flush() variant.
** 138 ECF 20090107 @41015 Minor update to stateChanged().
** 139 ECF 20090112 @41163 Fixed undo-related locking behavior. Locks
** were being downgraded rather than released in
** certain transaction-boundary cases.
** 140 ECF 20090128 @41240 Added stale flag and isStale(), setStale().
** A buffer is stale when the contained DMO's
** state is known to be out of sync with the
** corresponding record in the database. This
** can occur after a database-level rollback.
** 141 ECF 20090202 @41249 Fixed setCurrentRecord(). Release/downgrade
** of lock on outgoing record was not properly
** handled in transaction cases.
** 142 ECF 20090206 @41264 Major changes to invocation handler and auto-
** commit processing. Batch mode was modified in
** order to enable an auto-commit transaction to
** begin at the first update and end in the
** endBatch() method. The copy() method was
** modified to not begin a new transaction if
** already in an auto-commit batch. The proxy
** invocation handler now attempts to associate
** a detached record with the current Hibernate
** session before invoking an indexed setter or
** getter method, to avoid a lazy initialization
** exception.
** 143 ECF 20090218 @41332 Memory improvements. Avoid creating
** unnecessary Reversible maps. Set reduced
** default sizes on certain collections.
** Leverage cached collections of getter methods
** in PropertyHelper.
** 144 ECF 20090306 @41437 Fixed load(). Added code to prevent releasing
** a newly created (still transient) record when
** trying to re-load the same record.
** 145 ECF 20090312 @41585 Fixed regression caused by #142 (@41264).
** Detached record association logic inside
** invocation handler could try to reassociate a
** null record.
** 146 ECF 20090315 @41607 Fixed ReversibleDelete.getDMO(). In the event
** target DMO is currentRecord, ensure lazily
** initialized extent field collections have
** been realized before returning DMO. Minor
** optimization to load() to avoid reloading the
** same temp table record unnecessarily during
** undo processing.
** 147 GES 20090424 @41942 Import change.
** 148 ECF 20090508 @42127 Rewrote validation processing. Validation is now
** much more incremental: only those indexes and
** properties which have been modified are validated
** immediately for new records. Remaining validation
** occurs when the buffer is flushed.
** 149 ECF 20090512 @42155 Fixed regressions caused by #148 (@42127).
** Integrated BufferFlushQueue.
** 150 ECF 20090513 @42167 Added isPropertyIndexed(). Needed for validation.
** 151 SVL 20090225 @41364 API change. recordID() returns recid instead of
** integer.
** 152 ECF 20090610 @42633 Added evictDMOIfUnused(). Evicts DMO from
** Hibernate session if no resource references it.
** 153 ECF 20090611 @42660 Optimized load() method and ValidationHelper.
** We now avoid expensive database operations and
** validation, respectively, when possible.
** 154 ECF 20090612 @42666 Fixed regressions in #153 (@42660). Partially
** rolled back changes to load() method.
** 155 ECF 20090623 @42963 Made changes to use updated BufferManager API.
** Some methods to retrieve undoable/reversible
** actions were changed.
** 156 SVL 20090706 @43054 Into copy() catch and rethrow all
** ConditionExceptions without rolling back
** transaction.
** 157 ECF 20090707 @43104 Fixed validation corner case. A newly created,
** permanent record with dirty properties but no
** dirty indexes was being validated and flushed,
** but not shared with other sessions. Later, an
** attempt to rollback dirty changes failed.
** 158 SVL 20090706 @43062 Into endBatch() rollback transaction if
** validation has failed.
** 159 CA 20090709 @43126 When batch mode ends, if validation fails, the
** reversible info must be collected (to allow
** rollback for the changed values). On rollback,
** a ReversibleUpdate must actually rollback the
** changed property, regardless of the mode (re-roll
** forward or not full transaction block).
** 160 ECF 20090716 @43222 Refactored initialization logic. Since it is
** possible to define a buffer and even open a scope
** on it without the backing database being
** connected, initialization of many critical
** instance variables cannot be set up during
** construction. This logic was moved to the new
** initialize() method, which is invoked lazily, as
** late as possible, within and external to this
** class.
** 161 ECF 20090717 @43229 Fixed regressions in #159 and #160. Backed out
** changes to ReversibleUpdate.rollbackWorker, which
** can cause unique constraint violations. Buffer
** must be initialized when invocation handler is
** called for a DMO method.
** 162 ECF 20090720 @43306 Improved ValidationHelper. Changed prevalidate
** method to prevent unnecessary, future validation
** in some cases.
** 163 CA 20090724 @43382 Rolledback H140 (#41240), to fix the rollback
** mechanism. Also, on rollback, the DMO's which
** have reversible actions will be processed in
** reverse order instead the order they were
** registered.
** 164 CA 20090724 @43395 On query off end, the undoables registered with
** this record buffer are not undone - the buffer
** must not reference any records after a query
** which uses the buffer goes off-end.
** 165 ECF 20090725 @43422 Fixed validation and flushing of temp tables in
** invocation handler. Added getMasterBuffer()
** method to support proper rollback handling of
** shared temp tables. Prevent prevalidation in some
** cases.
** 166 ECF 20090729 @43449 Implemented Commitable.validate() and fixed
** flush(). The former is needed to validate and
** flush transient records when field assignments do
** not trigger flushing. The persistence and change
** reporting within flush() is now bracketed with an
** explicit transaction to prevent auto-commit temp
** table records from leaving behind a pending flush
** entry in the ChangeBroker. This was causing flush
** problems and downstream data loss.
** 167 SVL 20090803 @43477 Raise stop condition into initialize() if the
** database isn't connected.
** 168 CA 20090804 @43508 Added implementation for reset() method in
** QueryOffEndListener - this will set the buffer to
** reference no record.
** 169 CA 20090807 @43557 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.
** 170 SVL 20090810 @43578 Into Handler.invoke() log
** IndexOutOfBoundsException stacktrace only as
** debug output.
** 171 ECF 20090810 @43581 Fixed getDatabaseAndTable(). This method must not
** throw IllegalStateException if the buffer is not
** yet initialized. Instead, it must return null.
** 172 ECF 20090810 @43590 Changed to accommodate SessionListener API
** change. sessionClosing() method was replaced with
** sessionEvent().
** 173 ECF 20090812 @43603 Replaced getDatabaseAndTable() with
** getBufferType(). More information was required by
** callers than was provided by the database and
** table string, so the BufferType class is used
** instead.
** 174 ECF 20090813 @43615 Fixed validation problem which caused a flushing
** error downstream. When validating dirty props, we
** now undo them immediately upon failure, so they
** are never flushed to the database nor recorded
** in Reversibles. Failure to do this was causing
** unique constraint failures in some cases.
** 175 ECF 20090815 @43634 Fixed invocation handler range checking for
** extent field access. Element index is now checked
** for unknown value and out of bounds conditions
** before backing method is invoked.
** 176 ECF 20090819 @43691 Fixed ReversibleUpdate.rollbackWorker(). This
** method was making an assumption that the buffer
** still contained a snapshot of the record being
** rolled back. It now uses a different API into the
** dirty share context.
** 177 CA 20091001 @44059 Fixed leak caused by bulk delete duplication: do
** not duplicate it if the record was deleted
** explicitly (via a DELETE operation) or implicitly
** (via a bulk delete).
** 178 CA 20091001 @44081 Avoid NPE in rollback(). In certain bulk delete
** cases, the Reversible will not return a DMO, so
** we cannot make a deep copy for the snapshot.
** 179 SVL 20091019 @44144 Added support of assign and delete database
** triggers.
** 180 SVL 20091019 @44159 Pass a duplicate of the old value into assign
** triggers in order to ensure that changes into
** this value will not affect undo processing.
** 181 ECF 20091026 @44192 Fixed compare() method. Need to use getter method
** specific to each DMO's buffer, in case the types
** are different. We were using the getter method
** for only the first DMO, which made an implicit
** (and incorrect) assumption that both DMOs were of
** the same type.
** 182 CA 20091102 @44300 Fixed retry processing - on retry, the openScope
** call must not be executed.
** 183 SVL 20091117 @44402 Notify the BufferManager when a new record was
** loaded (or the old record was reloaded) into the
** buffer.
** 184 CA 20091202 @44467 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.
** The ReversibleChain is no longer used, instead a
** linked set with all operations which affect a
** certain table is used.
** 185 ECF 20100128 @44553 Fixed NPE in rollback(). Added safety code to
** handle certain bulk delete case.
** 186 SIY 20100429 @44837 Fixed double rollback in some conditions.
** 187 CA 20100527 @44880 Added support for CREATE and "no record is
** available triggers". The CREATE, DELETE and
** ASSIGN triggers will execute too, but the passed
** DMO will not be the buffer proxy; instead, it
** will be the actual record instance.
** 188 CS 20121031 Removed client specific references
** 189 CA 20130117 Added possibility to retrieve the DMO interface
** from the proxy's invocation handler.
** 190 ECF 20130119 Changed the public API to this class; a number of
** static methods have been removed and replaced
** by the Buffer interface API. This enables more
** natural, direct method calls on DMO proxy objects
** for standard services. This entailed a change to
** the DMO proxy implementation from the dynamic
** proxy support in java.lang.reflect to that in
** com.goldencode.proxy (we needed to inherit our
** proxies from a functional base class).
** 191 CA 20130126 Renamed getDMOInterfaceForProxy to
** getDMOInterfaceForObject and fixed possible
** ClassCastException and made it to receive any
** kind of object.
** 192 OM 20121106 Added CURRENT-CHANGED built-in function implementation.
** 193 OM 20130212 Added CREATE-BUFFER conversion support.
** 194 ECF 20130224 Added limited support for embedded SQL.
** 195 OM 20130304 Added missing create() (CREATE-BUFFER) signatures and ordered the
** existing ones.
** 196 VMN 20130428 Implemented runtime support for handle-based methods BUFFER-COMPARE
** and BUFFER-COPY, reusing code with corresponding statements. Stub
** added for compare() method containing FieldReference as last
** parameter. Added CASE-SENSITIVE option conversion and runtime
** support for the statement version of BUFFER-COMPARE
** 197 SVL 20130331 Upgraded to Hibernate 4.
** 198 OM 20130416 Added reminder for integer (maybe other) validation fails.
** Scanned InvocationTargetException in invoke(Object, Method, Object[])
** for ErrorConditionException and act as P4GL.
** Added support for int64 expression indexes.
** 199 VMN 20130711 Fixed properties comparing with extent: isAssignableTypes().
** 200 OM 20130702 Fixed 'unmappable character for encoding ASCII' error
** 201 VMN 20130722 Added aggregated functions support for embedded SQL.
** 202 VMN 20130903 Added table lock to deleteAll().
** 203 OM 20130829 Finished/fixed current-changed implementation.
** Method areDMOsSame() is now package visible.
** 204 CA 20130918 Added widget-pool support. The pool name must be validated before
** anything else, and a null name means the default pool is used.
** 205 SVL 20130822 Added isDynamic, changed some access modifiers.
** 206 ECF 20130509 Commented out validation error message about failure to update a
** field from invocation handler. We may need to put this back under
** certain conditions at some point, but for now I can't recreate the
** circumstances which must have prompted me to first put this in.
** Added base metadata support.
** 207 ECF 20131028 Added lock metadata support. Made aggregate function-related methods
** package private instead of public.
** 208 SVL 20131028 Added legacyName and blockDepth parameter to constructors and
** define(), Added openScopeAt. Implemented create() functions.
** 209 CA 20131209 Added support for INSTANTIATING-PROCEDURE. Fixed implicit deletion
** of static buffers.
** 210 VMN 20131212 Fixes in deleteAll(): using java annotations for defining primary
** index and NPE (initialize() moved)
** 211 OM 20131128 Implementation of database trigger firing.
** 212 SVL 20140106 Fixed copy(), added validate(Persistable, boolean).
** 213 OM 20140607 Fixed double-call of finishedImpl() call when processing Finalizables
** from TransactionManager. Added read/only getter.
** 214 SVL 20140124 getResource is used for unwrapping handles.
** 215 ECF 20140303 Fixed copy/compare methods called from converted handle-based methods
** to use legacy field names and added error handling for many error
** conditions. Replaced Apache commons logging with J2SE logging.
** 216 SVL 20140311 Allow existence of NO-UNDO records which violate constraints.
** 217 VMN 20140325 Added support for custom denormalization of fields with extent.
** 218 CA 20140406 Fixed lock downgrade for buffers surviving a persistent procedure.
** 219 VMN 20140409 Fixes in BUFFER-COMPARE support, including using legacy field names.
** 220 OM 20140417 Fixed typo in method name (addToAllBuffersArray).
** 221 ECF 20140416 Fixed bug in getPropsMap.
** 222 OM 20140508 Added package access getter for the value of a requested field
** (needed by DMOValidator when computing the index-key sizes).
** Added index-key validation in validateRemaining().
** 223 ECF 20140613 Renamed getDMOInterfaceForObject to getDMOBufInterfaceForObject and
** changed it to return the combined DMO and Buffer interface, rather
** than just the DMO interface.
** 224 ECF 20140703 Use current block depth in createDynamicBufferForPermTable instead of
** -1. This ensures buffer is registered properly with buffer manager.
** 225 VMN 20140723 Fixed BUFFER-COPY method when pairs-list maps concrete index of field
** with extent to the field without extent.
** 226 SVL 20140725 Fixed depth at which a buffer was opened in
** createDynamicBufferForPermTable.
** 227 VMN 20140806 Fixed BUFFER-COPY and BUFFER-COMPARE methods when pairs-list maps
** fields with equal and different extents or concrete index of field
** with extent to the field without extent. Method getExtent(...)
** corrected.
** 228 ECF 20140801 Major rework around trigger support and the validation and flushing
** algorithms. Changed the timing of implicit validation to occur at the
** earlier of the end of the current, full transaction or the current
** buffer's outermost scope. This had major repercussions on the dirty
** sharing algorithms as well. Eliminated use of BufferFlushQueue, which
** was flushing records too aggressively.
** 229 ECF 20140930 Log a warning if there is an error during rollback in flush method.
** 230 OM 20141002 Fixed getter field proxy to return correct type of UNKNOWN value.
** 231 OM 20141003 COPY and COMPARE buffer operations use legacy names for pairing
** fields to be copied.
** 232 OM 20141015 compare returns single element for EXTENT fields.
** Removed locking on temporary readonly buffers in Handler.invoke().
** 233 VMN 20141022 Corrected equals() and hashCode() methods for DatumAccess.
** 234 OM 20141029 Added defineAlias() and support for defining buffer parameters and
** implemented stacking of buffer names (property & legacy).
** 235 ECF 20150101 Fixed createDynamicBufferForPermTable and callers to use the logical
** database name instead of physical database name when defining a new
** buffer.
** 236 OM 20150112 Added special care of buffers when it is open inside a persistent
** procedure. They will be stored in global scope of the local
** ChangeBroker and will be removed when the procedure is deleted.
** 237 ECF 20150127 Fixed aggregate method, which was not preprocessing the HQL where
** clause, and thus was not dealing with query substitution parameters
** properly.
** 238 VMN 20150228 Fixed array setter reference for denormalized field for simple value.
** 239 VMN 20150314 Fixed new warning when compiling.
** 240 VMN 20150317 Added check isJavaBeanMethod() in method getDenormalizedMethod().
** 241 OM 20150303 Chained dynamic buffers into HandleChain.
** getLDBName() returns null for aliases to unconnected databases.
** 242 ECF 20150323 Deregister dynamic buffers with the buffer manager when they are no
** longer in use; other performance improvements.
** 243 VMN 20150403 Fixed array setter reference for denormalized field for array value.
** 244 OM 20150504 Fixed registration of buffers belonging to procedures running in
** PERSISTENT mode in global scope of the ChangeBroker.
** 245 ECF 20150517 Fixed global scope cleanup, rearranged methods by access modifier.
** 246 ECF 20150801 Added support for compound query optimization. Initial fixes for
** runtime denormalized schema support.
** 247 ECF 20150906 Reimplemented temporary silent error handling of query substitution
** parameters. Changed DMO in-use tracking to match changes in
** BufferManager API. Added isUnknownMode for outer join support. Fixed
** buffer-copy transaction leak and field pairs flaws.
** 248 GES 20150923 Javadoc fix.
** 249 ECF 20150929 Optimized certain frequently invoked Persistence methods.
** Reimplemented invocation handler to no longer be a SessionListener,
** eliminating most of the SessionListener notification traffic.
** 250 ECF 20151013 Clarified defineAlias javadoc.
** 251 ECF 20151031 Modified getLogicalDatabase to initialize buffer.
** 252 OM 20151026 Fixed flush() when UNDO attribute has changed (temp-tables only).
** Marked record as persisted on [create] rollback merge.
** 253 SVL 20151119 Close related queries when buffer's scope ends.
** 254 ECF 20151208 Temporary fix to prevent NPE in toString(Persistable). NPE is gone,
** but output is still not quite right for temp-table buffers; needs
** further attention, but is not high priority.
** 255 ECF 20151214 Created package private throwOffEnd method.
** 256 ECF 20160105 Allow getDatabase to be called before a buffer is active.
** 257 ECF 20160113 Fixed case of processing undo at the end of a block when the record
** in the buffer hasn't changed. We should not retrieve the record from
** database in this case.
** 258 ECF 20160216 Fixed denormalized schema support. Modified and significantly
** streamlined invocation handler to work with improved DMO proxies.
** Now, the only getter/setter methods which are passed to the
** invocation handler are simple bean methods, plus one variant each of
** an indexed getter and setter, which take a simple int value as the
** subscript parameter. Mapping of a legacy-style, indexed call to a
** denormalized extent field is now handled by the DMO proxies directly,
** even those method variants which accept or return arrays, or assign a
** scalar value to all elements in an extent field. See DmoProxyPlugin
** for additional details.
** 259 ECF 20160321 Cache hash code at construction and override hashCode and equals
** methods to be final. Ensure reversibles are not merged to parent
** scope if that would be outside of the buffer's scope.
** 260 OM 20160302 Adjusted the listDirtyIndexes() and getDirtyProperties();
** cumulativeDirtyProps also keeps the changes.
** ECF 20160430 Fixed ChangeBroker registration in openScopeAt. Do not include dirty
** DMOs in DMO reference counting.
** 261 ECF 20160512 Avoid TM.deregisterGlobalFinalizable warning when global scope is
** finished. Modified deleteAll to deal with null primary index.
** 262 CA 20160518 Change ParameterIndices usage to collect all substitution parameters,
** as their number might change (up or down) after ternary is normalized.
** 263 ECF 20160605 Reduce unnecessary flushing and other minor performance fix.
** 264 ECF 20160615 Performance tweaks.
** 265 OM 20160629 HQLPreprocessor needs the list of fields of the current index.
** 266 IAS 20160701 Added getLegacyName() method, fixed temp table lookup.
** 267 IAS 20160714 Use legacy name instead of DMO alias.
** 268 OM 20160715 Added support for template records.
** 269 ECF 20160720 Fixed buffer lookup during HQL preprocessing.
** 270 CA 20160627 Reworked undoable support - the undoables register themselves with
** all blocks up the stack, until either 1. the tx block which created
** it is reached or 2. the last tx block where it was saved is reached.
** 271 ECF 20160823 Changed locking for improved concurrency, disabled primary key
** reclamation; improved detection of records deleted in other
** contexts.
** 272 ECF 20160827 Fixed failure of buffer compare for some temp-table cases. Avoid
** calling RecordLockContext.relinquishBufferLocks inside a
** transaction.
** 273 ECF 20160829 Performance enhancements. Use local BufferManager to check
** transaction status instead of TransactionManager. Changed session
** session flushing and auto-commit behavior to allow better batching
** of database changes. Modified DMO eviction implementation.
** 274 GES 20160804 Minor javadoc update.
** 20160919 Changes to match query class constructor/initialization changes.
** 275 ECF 20161004 Use implicit transactions where possible to reduce flushing.
** 276 ECF 20161014 Fixed implementation of reload() method in the event there is no
** current record in the buffer to reload, and this is not considered
** an error in the context of the query (e.g., query represents an
** outer join and record can be null).
** 277 OM 20170202 Moved initialization of createScope before calling CREATE trigger.
** Implemented bulk delete for permanent tables.
** 278 ECF 20170325 Fixed lookupNumFields, which must use the legacy table name.
** 279 SVL 20171025 Added buffer-specific field attributes.
** 280 ECF 20171025 Made getDMOProxy and getParentTable public to enabled access from
** serial package.
** 281 ECF 20171128 If no lock type is provided for load method, use the currently
** pinned lock type for the record being loaded.
** 282 GES 20171207 Removed pending error checking to match new silent mode approach.
** ECF 20171212 Added a TODO for query substitution parameter error handling for
** aggregate queries (embedded SQL).
** 283 ECF 20171202 Omit DMO alias from HQL for a bulk delete.
** OM 20171212 HQLPreprocessor.get() signature change. Added delete() variant with
** support for external buffers for nested selects.
** ECF 20171217 Allow new bulk delete variant to be invoked iff supplementary DMO
** array is null.
** 20171219 Made isTransient public.
** CA 20171220 Added getUndoable.
** 284 EVL 20180207 Fix for invalid char in comment issue.
** 285 ECF 20180224 Fix for load: must register buffer with record lock context.
** 286 ECF 20180305 Buffer is read-only if underlying table is read-only.
** 287 OM 20180307 Added context local trigger awareness.
** 288 ECF 20180319 Fixed BDT format errors when detailed logging is enabled.
** 289 ECF 20180412 Fixed lookupNumFields to use legacy table name, not buffer name.
** 20180418 Fixed write trigger and flushing logic.
** 290 ECF 20180522 Fixed validation failure during record delete.
** 291 ECF 20180527 Force a pending flush for a non-transient, modified, validated
** record when a batch assignment is closed.
** 292 CA 20180520 Fixed dynamic buffer creation when the name is qualified with the
** schema.
** ECF 20180527 Force a pending flush for a non-transient, modified, validated
** record when a batch assignment is closed.
** 293 CA 20180621 Added recordNotAvailable[Type] - an API emitted for VALEXP where
** there are buffer references with invalid scopes.
** 294 ECF 20180710 Minor performance tweak to popPinnedLockScope.
** 295 CA 20180723 Batch assign mode emits as lambda instead of start/endBatch brackets.
** ECF 20180731 Ensure a temporary record placed in the buffer to satisfy client-side
** sorting or filtering is associated with the session (needed to avoid
** LazyInitializationException for collections used by extent fields).
** 296 ECF 20180809 Ensure template record is not attached to session by a lock request.
** 297 ECF 20180920 Ignore NonUniqueObjectException when ensuring DMO is associated with
** Hibernate session.
** 298 OM 20181022 Fixed CURRENT-CHANGED implementation (to ignore changes in WRITE
** triggers).
** 299 OM 20181031 Added BufferManager-level unknown mode support.
** 300 ECF 20181220 Fixed armCurrentChanged: ensure Hibernate session is bound before
** deep-copying current record.
** 301 OM 20190111 Added support for VST R/O checking. Using annotations to manipulate
** tables.
** 302 OM 20190202 Fixed VST R/O checking (including create and delete).
** 303 OM 20190117 Integrated _Trans metadata support.
** OM 20190129 Added ref-counter for objects store in fields.
** ECF 20190212 Refactored copy and compare methods. Added no-lobs implementation to
** same.
** ECF 20190311 Ensure DMO is bound to a Hibernate session before attempting deep
** copy of a record with lazy extent fields.
** ECF 20190314 Ensure we are deleting the correct DMO instance when rolling back a
** ReversibleCreate.
** ECF 20190317 Provisionally modified validate(boolean) to flush unconditionally for
** undoable buffers. Ensure new current record instance is stored, even
** if existing current record has the same primary key.
** ECF 20190322 Minor change to rollbackPending due to optimization in BufferManager.
** ECF 20190323 Avoid managing undoable resources if no changes have been made by a
** buffer instance.
** 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.
** ECF 20190326 Avoid assign trigger lookup for DMO getter method invocations.
** 304 ECF 20190414 More lazy initialization fixes. Prevent recursive deletions of the
** same record by cascading/nested delete triggers.
** 305 SVL 20190614 Added functions for obtaining code page of a field.
** 306 ECF 20190706 Treat any call to a BLOB/CLOB setter as a property change,
** regardless of whether the value has changed.
** 307 OM 20190510 Error 5365 is generated only from statements, not methods. Added
** support for before/after buffers. Implementes ROW-STATE, BEFORE-ROWID
** and AFTER-ROWID.
** 308 ECF 20190714 Added support for aggressive buffer flushing mode.
** 309 ECF 20190628 When undoing dirty changes, apply directly to current DMO, not to
** proxy.
** SBI 20190728 Added [get|set]FieldHelp.
** ECF 20190725 Distinguish "fake" buffers (i.e., those created as the read-only
** "old" buffer when a write trigger is fired) from other types of
** read-only buffers.
** ECF 20190813 Prevent write trigger from firing at inappropriate times.
** 310 AIL 20190821 Added property to tracker when a trigger is fired at the end of a
** batch.
** 311 CA 20190724 BUFFER-COPY must not process BEFORE-TABLE if the TRACKING-CHANGES
** flag is not set.
** ECF 20190802 Minor fix to error message. Fixed a legacy name regression caused by
** a change to BufferManager.addToAllBuffersArray. Fixed a leak of
** RecordChangeListeners registered with ChangeBroker.
** CA 20190812 Changes to allow for mutable buffers; any API which receives a Buffer
** instance and is invoked from converted code must resolve the runtime
** instance before saving the instance.
** ECF 20190824 Do not initialize the extent fields if this is a transient record.
** ECF 20190827 Added getSchemaDictionary to properly configure a context-local
** SchemaDictionary instance which can resolve database references for
** a specific list of buffers.
** OM 20190831 Added hidden fields and index specific to BEFORE TEMP-TABLES.
** ECF 20190915 Another fix to flush and write trigger invocation during validation.
** 312 OM 20190906 Added support for MERGE and REPLACE fill modes. Improved ERROR-STRING
** and ERROR-FLAG support.
** CA 20191009 Added where and sort clause translation in case of bound buffers.
** 313 CA 20191119 Track the executing legacy class, to be able to register any buffer
** to its source definition class.
** 314 AIL 20191128 Added validation between the delete trigger and effective delete.
** CA 20200110 Static buffers defined in an internal entry must be deleted upon exit.
** 315 AIL 20191213 Added ArgumentBuffer as a proxy used in case a buffer is referenced
** through a parameter buffer. Fixed proxy suppressing enclosed proxies.
** AIL 20200108 Prevented direct access to proxy members (without using the handler)
** 316 AIL 20200110 Fixed trigger calling to be done before updating the dirty context.
** 317 OM 20200120 Small typo fixes in javadoc.
** 318 AIL 20200410 Added methods for handling eviction avoiding for temp records.
** 20200415 Reverted eviction handling.
** 319 IAS 20200413 Fixed DMO instantiation on SESSION:DATE-FORMAT, minor change
** to validate() (conditional ValidationException throw)
** 320 AIL 20200611 Moved buffer manager's buffer loaded notify after record loading.
** 321 OM 20191101 Used new persistence API.
** 322 OM 20200623 Added persistentProcedure member which stores the procedure who
** opened this buffer for the first time.
** IAS 20200819 Added table CRUD ops statistics support
** GES 20200731 Eliminated context-local lookups by using ErrorHelper.
** ECF 20200909 Added variant of the release method which is invoked as part of an undo
** operation. Added handling for stale records. Reworked the logic in
** validate(boolean, boolean) logic to work better with the new ORM
** implementation.
** SVL 20200913 isTemporary became public.
** ECF 20200917 Backed out bracketing of DMO instantiation/copy with SESSION:DATE-FORMAT
** use, as it no longer had an effect with recent ORM changes.
** 20200919 We no longer force a refresh from the database when [re]loading a DMO.
** CA 20200922 Optimized performance of record copy (be it from parameter access or buffer-copy),
** by avoiding the RecordBuffer$Handler overhead, for non-indexed, non-mandatory,
** non-trigger properties - for these, the access (getter or setter) is done
** directly via the record's datum.
** OM 20200924 Avoid validation and flush of read-only DMOs.
** CA 20200924 Fixed a problem with CA/20200922 - when the DMO is a proxy, access to fields must
** be done via getter methods, to allow the proxy to de-reference the proper bound
** buffer instance.
** Replaced Method.invoke with ReflectASM.
** Replaced DMO property access from reflection to property's data handler.
** Added caches for the buffer's schema and data model ASTs.
** ECF 20200925 Added variant of getSession which creates session if needed.
** CA 20200927 Avoid context-local lookups by relying on the helper instance.
** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access.
** OM 20201002 Use DmoMeta cached information instead of map lookups.
** CA 20201007 Fixed buffer definitions from internal procedures/functions - the 'defining
** procedure' was incorrectly assumed as the caller, and not the currently executed
** external program.
** AIL 20201210 Added faster buffer-copy only for buffers which compatible explicit signatures.
** ECF 20201210 Reworked toString() to use legacy field names.
** AIL 20201223 Extended support for buffer-copy in fast mode.
** 20210104 Fixed assign trigger checking when doing fast buffer copy.
** 20210129 Setting active buffer before doing fast buffer copy.
** OM 20201120 Added DATA-SOURCE-MODIFIED and DECIMALS implementation. Made methods accessible
** to [serial] package. Fixed dataset events firing.
** OM 20201218 Fixed implementation for error and rejected attributes/hidden fields.
** OM 20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to
** local dmoMeta.legacyTable.
** ECF 20210117 Do not update DMO state when explicitly validating. Attempt to flush a transient
** record if still in the buffer when its scope ends.
** OM 20210223 Invalidate index-based cache in the event of low-level SQL operations.
** AIL 20210228 Allow markBatchDirty to mark this buffer as dirty in a batch context.
** ECF 20210301 Fixed NPE in toString(Record).
** OM 20210309 Do not use DmoMeta as key in TableMapper because temp-tables may share the same
** DmoMeta instance.
** EVL 20210319 Added method to get field value that can be updated at runtime. Will be used
** with higher priority over the legacy value.
** AIL 20210322 Fixed write trigger firing on validate statement.
** OM 20210323 Dropped the local field attribute map in favour of delegation to delegation to
** delegation to TableMapper.LFI.
** OM 20210404 CLOB fields are created and returned with their CP set.
** OM 20210419 After a record is successfully inserted in database table, the buffer storing it
** is removed from early inserts list of dirty context.
** IAS 20210419 _UserTableStat counters' update
** OM 20210429 Release the current record at the end of the scope and decrement the DMO use
** count to allow the persistence to evict it if not in use.
** ECF 20210504 BufferManager API name change.
** ECF 20210428 Fixed buffer-copy() pairs processing for individual extent field elements. Avoid
** unnecessary work in fastCopy for the quick-exit cases.
** ECF 20210506 Made dmoInfo private. All external access must be through its getter method to
** prevent NPE when accessing this information via a proxy. Removed unused
** tableStats variable and setter method.
** IAS 20210506 Added _UserTableStat-delete counter update,
** TODO for _UserTableStat support, fixed Javadoc
** ECF 20210511 Replaced DynamicTablesHelper.normalizeName with TextOps.rightTrimLower.
** ECF 20210519 Use new Record APIs to track DMO "in-use" status, instead of BufferManager.
** OM 20210521 Implemented trigger vetoing mechanism: if trigger return error CREATE and
** DELETE operations are aborted.
** OM 20210603 Implemented vetoing mechanism for ASSIGN triggers, too.
** ECF 20210612 Renamed hooks which are called upon opening/closing of outermost buffer scope
** from {open|close}ConnectionManagerScope to on{Open|Close}OutermostScope.
** OM 20210628 Default the legacy name to dmoInfo.legacyTable if name is null or unknown.
** ECF 20210805 Fixed nested calls to set and restore the current record's activeBuffer state.
** ECF 20210820 Retrofit for query substitution parameter preprocessing fixes.
** CA 20210920 When a buffer is deleted, all related queries must be reset (their predicate and
** buffer list).
** ECF 20210924 Prevent validation, flushing of a new/changed record while it is being deleted.
** Other minor refactoring.
** AL2 20211015 Use real referents instead of class defs when registering buffer.
** CA 20210929 Fixed cases when rowid function is used in the pairs for BUFFER-COPY or
** SAVE-ROW-CHANGES.
** AL2 20211022 Look-up def only if this should be registered as pending.
** ECF 20211122 Fix open buffer registration to register persistent procedure buffers at the
** global scope of the BufferManager.openBuffers scoped list.
** ECF 20211124 Rolled back previous fix. Let BufferManager determine the proper scope at which
** to track an uninitialized, open buffer.
** OM 20211209 Fixed regression when registering a buffer as a Finalizable with the TM.
** The dynamic buffers of TempTable are created at GLOBAL level.
** OM 20211015 Added datasourceRowid() method.
** CA 20211222 Fixed memory leak: remove the persistent procedure reference from the buffer,
** when it gets deleted.
** ECF 20211230 Remove a newly created record from the record nursery if it is deleted before it
** is flushed.
** ECF 20211231 Improved efficiency of last fix (only attempt to remove the deleted record from
** the nursery once).
** CA 20220104 Fixed memory leak: when deleting a buffer, clear it from BufferManager regardless
** if this is a dynamic buffer or not.
** An attempt to fix a leak from ConnectionManager.pdbReferences - in finishedImpl,
** if the buffer's scope is still 0 and is active, then call onCloseOutermostScope.
** AL2 20220119 Avoid the buffer-copy if the source and target have the same record instance.
** CA 20220201 Any instance field used from a static method must be accessed via a getter or
** setter, as the reference may be a proxy buffer (fixed currentRecord, dmoProxy and
** bulkCopy usage).
** IAS 20220218 Change visibility of the 'isUnknownMode' method to public.
** 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.
** OM 20220228 Added byOuterQuery flag for marking situations when the buffer is empty because
** it is set by an OUTER-JOIN table.
** OM 20220301 Merged [unknownMode] and [byOuterQuery] flags.
** IAS 20220324 Re-worked LockTableUpdater.
** OM 20220328 Count and return errors in [endBatch] method.
** OM 20220506 Fixed snapshot used for OLD buffer in a WRITE trigger. Fixed buffer preparation
** for buffer copy operation.
** IAS 20220506 Fixed 'formatProperty' for primitive arrays.
** SVL 20220518 Added resetSnapshot().
** OM 20220511 Fixed database trigger (WRITE and ASSIGN) regressions.
** 20220608 Allowed the record to be validated even if transient. The initial values of the
** fields might collide with existing records breaking the unique indexes.
** 20220629 In case of Buffer-copy, make sure the destination buffer is aware of the fact
** that its fields were altered and prepare it for the write trigger.
** OM 20220707 Shared meta-knowledge about fields and properties of a record was collected in
** DmoMeta to avoid duplicating their collection and storage.
** OM 20220718 Added associate(DataModelObject) method. Invoked WRITE trigger when a transient
** is flushed to make room for a newly created record.
** CA 20220720 Instance fields can not be accessed directly from static methods - their getter
** must be used, as the buffer instance may be proxied. Fixed OM/20220707.
** OM 20220809 Use a temporary virtual scope while invoking the db triggers to block the
** release() to be recursively invoked at the end of a trigger().
** 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 BufferManager's undoable support is no longer being used, so it was removed.
** 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 Added JMX instrumentation for buffer define. Refs #6814
** Use a Supplier for the proxy plugin instance, as when the proxy is already
** created, there is no need to use this instance. Refs #6822
** Do not access tableHandle() in the FWD runtime, get the TempTable instance
** directly. Refs #6826
** CA 20221010 Performance improvements - avoid the ProcedureData lookup, by keeping a parallel
** stack of this data for THIS-PROCEDURE. Refs #6826
** OM 20221012 The TriggerTracker are bound to DMO, not to the buffer they are contained into.
** OM 20221017 Moved {OM 20220809} changes from finishedImpl() to maybeFireWriteTrigger().
** OM 20221019 Limited the number of "Unimplemented fast copy" warnings to 10.
** OM 20221021 Quick out in create() if tableName is empty.
** CA 20221101 Accessing a setter while not in a TRANSACTION will raise error 7369.
** TJD 20220504 Upgrade do Java 11 minor changes
** OM 20221125 Added referenceOnly method.
** OM 20220603 All buffers are tracked if the table's TRACKING-CHANGES attribute is set.
** CA 20220613 registerBindingBuffer must use as procedure the SOURCE-PROCEDURE in case of
** OUTPUT and THIS-PROCEDURE in case of INPUT, for BIND parameters.
** OM 20220616 Fixed population of before-table in case of BUFFER-COPY operations.
** OM 20220909 Allow unchanged transient records to be flushed even if no trigger is fired.
** CA 20220909 The 'doNotProxy' method array must be a constant, otherwise it will leak at the
** ProxyFactory$CacheKey.DO_NOT_PROXY_HASH_CODES.
** ECF 20221009 Fixed a NO-WAIT lock case.
** CA 20221010 Performance improvements - avoid the ProcedureData lookup, by keeping a parallel
** stack of this data for THIS-PROCEDURE. Refs #6826
** OM 20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
** CA 20230110 'setterDatums' and 'getterDatums' are created only when they are first accessed
** (via their getter methods).
** Removed 'cumulativeDirtyProps', 'scopeNameStack' and 'uniqueProperties' as these
** are never used.
** 'indexedProperties' is created when is actually accessed.
** HC 20230116 Replaced some handle usages with the actual wrapped resources for performance.
** CA 20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD
** runtime.
** CA 20230123 isPropertyIndexed must have protected access, because is accessed from static
** methods.
** 323 ECF 20230301 Added isMutable to indicate that buffer represents a DMO generated from a scan of
** a mutable database.
** 324 CA 20230322 Do not modify the maps returned by 'LegacyFieldNameMap' in 'getPropsMap'.
** Fixed a case where BUFFER-COPY or BUFFER-COMPARE were using a 'tt1.r1,rowid(tt2)'
** pair, which was not resolving properly.
** 325 DDF 20230327 Added definitelyHasRecords().
** 326 OM 20230215 Handled collision of temporary buffer names by temporarily overridding them.
** Avoid NPE in createSimplePropsMap() when property's accessors are not found.
** 327 IAS 20230407 Fixed 'getPropsMap'.
** 328 SR 20230510 Changed persistent.list() to match the new signature.
** 329 CA 202300505 The H327 change in 'getPropsMap' was incorrect - root cause was resolution of
** 'recid' column when it was not qualified with a buffer name.
** 330 IAS 20230414 Make 'loadRecord' method public.
** 331 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 332 RAA 20230525 Disabled TriggerTracker for TemporaryBuffer.
** 333 CA 20230523 Performance improvement of the buffer fill operation - when possible (data type
** match), both source and destination buffers), use direct access to read and write
** the value. Extent fields are not supported yet.
** 334 CA 20230530 ArgumentBuffer must get the definition buffer from the proxy buffer instance, and
** not the RecordBuffer bound instance.
** 335 GBB 20230616 Log msgs performance improvements & syntax cleanup.
** 336 CA 20230724 Further reduce context-local usage.
** 337 ES 20230911 Add call to markChangeScope for destination buffer at buffer-copy
** 338 OM 20230915 Added support for generic session triggers.
** 339 AL2 20230926 Reworked definatelyHasRecords into fastHasRecords. This returns only if we can
** make a sure statement on the fact that the underlying table has records or not.
** 340 DDF 20231128 Modified reportValidationException() method to handle the SQLException
** based on the dialect used.
** 341 ES 20231203 Added lenientOffEnd parameter to setRecord method. To know when QQE exception
** should be thrown
** 342 RAA 20231220 Made isTemporary a field of this class. Replaced instances of isTemporary()
** with isTemporary.
** RAA 20231220 Fixed a case in which ReflectiveOperationException could have been thrown when
** a RecordBuffer was created through a proxy.
** 343 OM 20240117 Removed expired comment.
** 344 HC 20240222 Enabled JMX on FWD Client.
** 345 OM 20240201 Make sure the Write trigger is armed in case of fast-access copy operations.
** 346 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.
** 347 SB 20240311 Added the useOpenScope flag for armWriteTrigger. Refs #8404.
** 348 OM 20240319 Fixed a "merging (?)" error (duplicate if conditional).
** 349 CA 20240320 Avoid BDTs within internal FWD runtime. Replaced iterators with Java 'for',
** where it applies.
** 350 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.
** 351 CA 20240409 Fixed 'getPropsMap' used by BUFFER-COPY() method - it must compute the property
** name from the value of the corresponding src/dst field maps.
** 352 AI 20240402 Added check of index out of bounds for denormalized fields for getters or
** setters invoked on DMOs.
** AI 20240409 Added return value after ErrorManager.recordOrThrowError.
** 353 AI 20240416 Fixed NPE when getter without arguments is used.
** 354 AL2 20240409 Honor multiply referenced records (i.e. it is stored in more than one buffer)
** Al2 20240412 Notify the buffer if the referenced record was changed,
** so this can be reported as dirty. Also made converted RELEASE more aggresive.
** 355 EAB 20240417 Checked if batchAssignMode is true when the field for which we are trying to assign
** a value is mandatory and that value is null.
** 356 OM 20240412 Mutable attribute is inherited by meta tables.
** 357 CA 20240423 Reduce the context-local lookup for TriggerTracker APIs.
** 358 AL2 20240517 Make validateMaybeFlush protected to honor possible argument proxy over the buffer.
** 359 EAB 20240423 Added sqlTableContentJson method.
** 360 AL2 20240617 Register buffer as finalizable if initialized, but its scope was not opened.
** AL2 20240618 Changed initialize signature to include fromDefine parameter.
** 361 DDF 20240613 Do not release a record when closing the scope of the buffer if there is a DELETE
** trigger being executed, as the change of the record needs to be reported and the
** event released from the TriggerTracker.
** DDF 20240620 Simplify condition that checks if a DELETE trigger is executing.
** 362 AL2 20240701 Added isBufferScopeLeaking to detect exceptional cases of buffer scope leaks.
** 363 AL2 20240517 Eagerly delete the DMO if transient and released in UNDO mode.
** AL2 20240523 Use DirectAccessHelper instead of persistence delete.
** AL2 20240708 Refactored finishedImpl to extract logic in a clean-up procedure.
** 364 AL2 20240708 Lower logging level for initialization without scope being opened.
** 365 RAA 20240613 Added tenant and database id parameters when calling FastFindCache.getInstance.
** RAA 20240617 Rearranged ffCache parameters.
** RAA 20240808 Adjusted FastFindCache to be retrieved from the persistence context.
** 366 AD 20240321 Ordered the methods in doNotProxy by their name.
** 367 CA 20240723 Added 'updateCachedRecords', to apply some code to cached temp-table records.
** 368 CA 20240809 Skip using JMX timers when JMX_DEBUG flag is not set.
** 369 CA 20240813 Assume OffEnd.NONE for a buffer just opening its scope, as no query is assumed
** of being active on the buffer, when its outermost scope is opened.
** 370 DDF 20240305 Made validateMaybeFlush() method protected to be invoked over the proxy,
** avoiding a NPE as proxies do not have a persistenceContext.
** 371 CA 20240827 Notify that a record has been flushed - for non-temp records, this will allow
** invalidation of the FastFindCache for that DMO.
** 372 OM 20240901 Improved Database API. Concurrent multitenancy functionality implementation.
** 373 CA 20230924 Further reduce context-local lookup.
** 374 TJD 20221130 Prevent recursive Invoke calls of creatingBuffer handler.
** TJD 20230329 Reset offEnd on the end of FOR/FOR EACH so other queries could continue iterating
** TJD 20230417 Allow buffer locks rollback on full rollback
** TJD 20230508 Updates needed to support dirty records leaks to other sessions on FIND
** TJD 20230824 Dont use TransactionManager, use txHelper instead
** TJD 20240110 Fixes for IndexOoB in toString
** TJD 20240110 Fixes for DirtyShare cross session.
** TJD 20240308 Call to shareDirty is not needed anymore
** TJD 20240315 Moved extractDMOInterface to RecordBuffer
** TJD 20240411 Updated 20230329 change to initialize offEnd at beginning of each iteration
** TJD 20240619 Fixed check for array length in formatProperty
** TJD 20240626 QueryOffEndListener needs also to reset RecordBuffer.snapshot
** AL2 20240710 On delete, remove the record from record nursery no matter its state.
** TJD 20240730 Allow offEnd.FRONT to be treated like offEnd.BACK for prev/last queries
** TJD 20240821 Get FastFindCache from the persistence context fix.
** 375 AS 20241009 Added support for validating a subset of unique indices.
** AS 20241009 Reworked setting an active buffer for the batch assign to be performed
** only once for every buffer inb the batch.
** AS 20241009 Moved the restoring of the active state for the dirty batch
** asign buffers to cleanupBatchMode().
** 376 AL2 20241023 Manage the scoped registration of this buffer to buffersByDmoClass collection.
** 377 AL2 20240829 Removed dirty context work from setter handler.
** AL2 20240903 Reworked recordInserted to no longer honor dirty share manager. The upcoming
** nursery remove will do the job.
** AL2 20241112 Adapt to new nursery remove signature.
** 378 AP 20241009 Added new method to reset the sortIndex.
** 379 AOG 20241121 Updated condition of currentRecord.setActiveBuffer only when invoking setter.
** 380 AS 20241127 Set the active buffer for CLOB getters.
** 381 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** Implementation of [setMultiTenantAlternative] method.
** 382 ES 20241125 Added additional check for buffer flushing in case a buffer is not undoable
** and not in the context of a agressiveFlushing.
** 383 ICP 20241202 In copy, added new error throw if invoke fails.
** 384 OM 20250114 Small optimization.
** 385 AP 20250129 Removed sqlTableContentJson method as its logic was moved to a specialized class.
** 386 ES 20250130 Changed the method call from generateUniqueBufferName to generateBufferName.
** ES 20250205 Added token variable to uniquely identify the buffers.
** 387 DDF 20250131 Added createProxy() used to create a proxy in case of a passed buffer parameter
** that matches the expected type (two dmos, but the same temp-table), while the
** existent proxy could not be "casted".
** 388 AS 20250220 Do not restore the active state of the buffers in copy/fastCopy.
** 389 ICP 20250129 Used logical.TRUE and character.EMPTY_STRING to leverage cached values.
** 390 AS 20250220 Refactored validateMaybeFlush to report only if the current property is changed.
** 391 AP 20250226 Optimized record equality checking in updateCurrentChanged.
** 392 CA 20250303 Fixed a NPE in 'FIND CURRENT EXCLUSIVE-LOCK', when the current record was deleted.
** Added state-changed 'refresh buffers' event, to force reload of all buffers in
** current context, for the specified DMO.
** 393 OM 20250130 Added createMeta() for internal usage (backstage meta table management).
** 394 OM 20250130 Added support for readonly meta tables.
** 395 OM 20250410 Disable R/O flag for admin meta buffers.
** 396 LS 20250403 Updated getSession() to handle the PersistenceException using DBUtils.
** 397 ES 20250407 Use timeout parameter in the call to Persistence.load.
** 398 CA 20250423 A buffer can be passed to a method call, which has a different DMO iface,
** because of resource-level annotations like XML-NODE-NAME. Emit a
** 'RecordBuffer.bufferProxy' for this case, which will create a proxy.
** TODO: implement 'bufferProxy.
** 399 ES 20250509 Make ArgumentBuffer and bufferProxy proxies classes to have as a super class the proxy class
** from previous level and no plugin, as it would infer the methods for extent
** fields from super class.
** 400 ES 20250508 Synchronize the curent buffer in case AUTO-SYNCHRONIZE attribute is set on true for
** CREATE, DELETE, RELEASE, BUFFER-COPY operations.
*/
/*
** 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.lang.reflect.Array;
import java.sql.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import org.apache.commons.lang3.tuple.*;
import org.roaringbitmap.*;
import com.goldencode.p2j.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.types.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.persist.trigger.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.proxy.*;
import com.goldencode.util.*;
/**
* A container for a Data Model Object (DMO) instance which represents a
* single database record (or the lack thereof). A record buffer provides an
* interface for the persistence framework to deal with many different DMO
* types in a consistent manner. It exposes several static methods which
* permit client code to define a new buffer and to manipulate the data
* record which backs that buffer.
* <p>
* Client code does not manipulate DMO implementation class instances
* directly, nor does it manage instances of <code>RecordBuffer</code>
* directly. Rather, DMOs are exposed to client code by proxy. Each DMO
* defines a business-domain interface, which is the means by which client
* code manages data records. A new buffer is defined using the {@link
* #define} factory method. This method returns a dynamic proxy to a
* particular DMO interface; however, the proxy initially is not backed by
* any DMO implementation instance. A backing instance is only created by
* either invoking the {@link #create} method, or by passing the proxy to
* a query and successfully retrieving a data record. Prior to the creation
* of a backing DMO implementation instance in a buffer by one of these
* means, any invocation of the DMO interface methods against the proxy will
* result in an exception. Likewise, if any attributes of the buffer which
* require a backing record are manipulated before such a record is available,
* an exception is thrown.
* <p>
* The dynamic proxy created at buffer definition actually implements more
* than just the DMO-specific interface. It also implements the {@link
* BufferReference} interface. This package private interface allows other
* persistence framework classes to access the record buffer associated with
* a DMO proxy, when the proxy is passed in as a parameter from client code.
* The proxy {@link RecordBuffer.Handler invocation handler} will detect
* method calls destined for this interface and will retrieve the record
* buffer object reference.
* <p>
* The primary responsibility of the invocation handler, though, is to
* dispatch DMO interface getter and setter method calls invoked against the
* proxy, to the correct backing DMO. This decoupled architecture is
* leveraged to provide support for the Progress early constraint checking
* semantic, whereby not-null and uniqueness constraints are checked at
* data value assignment time, rather than at record commit time. We also
* take advantage of this hook to perform reverse foreign record resolution.
* <p>
* Reverse foreign record resolution is the process whereby a foreign record
* associated with the current record DMO must be updated at runtime, as
* application code modifies the properties which represent the database
* fields upon which Progress relied as a foreign key. During conversion,
* natural joins between tables are detected and any legacy foreign keys are
* replaced with a foreign key which refers to the foreign table's surrogate
* primary key. Since this explicit foreign key reference did not exist in
* the pre-conversion Progress code, we must maintain relational integrity by
* manually updating this foreign key as the legacy key values change. This
* is the job of {@link AssociationSyncher} and its subclasses. The DMO
* proxy invocation handler is used to detect when the value of such a legacy
* key is modified, and the foreign key is reset immediately, unless the
* current context is running within an assignment batch (see below where
* batch processing is described with respect to validation). In the case of
* an active batch, the foreign record resolution is deferred until the batch
* ends.
* <p>
* In some cases, client code must make multiple updates to a DMO instance,
* and validation must be deferred until the last update is made, where it
* would not be meaningful to validate while the DMO is in an intermediate
* state. Batch assign mode is designed to handle this requirement. Once
* batch assign mode is {@link #startBatch started}, validation for all
* buffers in the current context is deferred until batch mode is {@link
* #endBatch ended}. At this point, all buffers which were updated during
* batch assign mode are validated, in the order in which they were updated.
* <p>
* Each new instance registers a lightweight implementation of the
* <code>Undoable</code> interface to enable the {@link
* com.goldencode.p2j.util.TransactionManager TransactionManager} to preserve
* the state of the buffer, such that it can be restored in an undo situation.
* <p>
* This class implements the <code>Finalizable</code> interface, so that it
* can release locks and records when a buffer scope closes. The actual time
* of release also depends upon the current state of the transaction manager.
* <p>
* This class implements the <code>Commitable</code> interface to enable the
* {@link com.goldencode.p2j.util.TransactionManager TransactionManager} to
* commit database transactions and subtransactions.
* <p>
* Upon instantiation, a buffer is associated with a logical database. From
* that point on, it can only be used with that database instance.
* <p>
* In certain circumstances (for outer join queries, particularly), a record
* buffer may not have a database record loaded, but must still be able to
* service proxied, DMO method calls without failing. <i>Unknown mode</i> is
* intended to support this need. A record buffer can temporarily be placed
* into this mode, such that getter methods invoked on the associated proxy
* return unknown value, instead of raising an error condition. This mode
* only is used internally by this package.
* <p>
* A similar feature is <i>temporary record mode</i>. Not to be confused
* with temp-table support (see {@link TemporaryBuffer}), this mode is used
* in certain cases where the buffer must temporarily hold some record data,
* in order to allow evaluation of an expression involving the associated
* DMO. This is done, for instance, when presorting preselected query
* results, and when filtering query results against a client-side where
* clause filter. This mode only is used internally by this package. The
* query invokes this mode by invoking the {@link #setTempRecord} method first
* with a non-<code>null</code> value, then executing the target expression
* possibly repeating this sequence multiple times, and finally clears the
* mode by invoking the <code>setTempRecord</code> method again, this time
* with a <code>null</code> argument. Setting the buffer's temp record does
* not trigger all of the locking and other baggage normally associated with
* setting the buffer's current record.
* <p>
* TODO: combine many boolean flags into a bit field.
* <p>
* TODO: currently, when there are multiple buffers defined for the same
* table, the BufferManager keeps together all reversibles related
* to the same table belonging to the same database. This ensures
* proper reversible processing on rollback.
* <p>
* But, the rollback related part in {@link Commitable} can be reworked
* to allow multiple buffers for a certain table in a database. This
* will eliminate the small overhead of rolling back a buffer which
* has nothing to do (rollback was performed by one of its siblings).
* <p>
* Note: a case which needs to be taken into consideration and checked
* is when the foreign-keys are enabled. In this case, if the child is
* rolled back first in a Child.Parent relation, there might be some
* problems.
*
* @see CompoundQuery
* @see PreselectQuery
* @see PresortQuery
* @see RandomAccessQuery
*/
public class RecordBuffer
implements BufferReference,
RecordChangeListener,
Commitable,
Finalizable
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(RecordBuffer.class.getName());
/** Log message. Stored as constant to avoid regular concatenation. */
private static final String LOG_MSG_SIGN_BROKEN = "Explicit signature checking is broken; unsuccessful " +
"fast buffer copy. Fallback to classic implementation.";
/** Log message. Stored as constant to avoid regular concatenation. */
private static final String LOG_MSG_SIGN_UNIMPL = "Unimplemented fast copy for explicit signature " +
"match, but different field order. Fallback to classic implementation.";
/** Log message. Stored as constant to avoid regular concatenation. */
private static final String LOG_MSG_DB_UNEXP_REF = "Buffer '%s' used in database expression references " +
"unexpected database schema '%s'. Please check 'default-databases' configuration";
/** Instrumentation for {@link #defineReadOnly} and {@link #define}. */
private static final NanoTimer DEF_BUFFER = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmBufferDefinePerm);
/** Array of methods not to be proxied */
private static final Method[] doNotProxy;
/** Array of methods not to be proxied from the {@link HandleResource}. */
protected static final Method[] doNotProxyRes;
static
{
List<Method> doNotProxyList = Arrays.asList(Buffer.class.getMethods());
Collections.sort(doNotProxyList, (m1, m2) -> m1.toString().compareTo(m2.toString()));
doNotProxy = new Method[doNotProxyList.size()];
for (int i = 0; i < doNotProxyList.size(); i++)
{
doNotProxy[i] = doNotProxyList.get(i);
}
try
{
doNotProxyRes = new Method[] { HandleResource.class.getDeclaredMethod("processResource") };
}
catch (NoSuchMethodException |
SecurityException e)
{
throw new RuntimeException(e);
}
}
/** Possible buffer operation types */
protected enum OperationType
{
/** Compare operation */
COMPARE,
/** Copy operation */
COPY
}
/** Possible method types */
protected enum MethodType
{
/** Statement */
STATEMENT,
/** Method */
METHOD
}
/** Context-local ChangeBroker */
protected final ChangeBroker changeBroker = ChangeBroker.get();
/** Object which manages this and all buffers in the current context */
protected final BufferManager bufferManager = BufferManager.get();
/** The database trigger manager for this context. */
protected final DatabaseTriggerManager dbTrigManager = DatabaseTriggerManager.get();
/** Transaction helper object */
protected final TransactionManager.TransactionHelper txHelper = bufferManager.getTxHelper();
/** Block depth at which outermost scope was opened */
protected int activeScopeDepth = -1;
/** Number of open scopes for this buffer (open scopes can be nested) */
protected int openScopeCount = 0;
/**
* If this buffer was ever registered with a persistent procedure, remember the scope it was
* open at (-1 in fact because the assignment is performed (and later compared) before
* incrementing the {@code openScopeCount}). If never registered with global scope it stays -1.
* It is used to unregister from the global scope of ChangeBroker when {@code openScopeCount}
* reaches back to this value.
* This flag only applies to static buffers from procedures run in PERSISTENT mode.
*/
protected int registeredWithGlobalScope = -1;
/**
* Flag indicating the finalizable processing for this buffer will be delayed for when the
* external procedure gets deleted
*/
protected boolean delayed = false;
/** List of queries which should be closed when the scope of this buffer is closing */
protected Set<P2JQuery> relatedQueries = null;
/**
* If {@code true} this flag makes the change-tracking of this dataset AFTER-BUFFER ignore
* changes and skip storing changes to associated BEFORE-BUFFER. This is needed by
* REJECT-CHANGES when CHANGE-TRACKING is still on. Although the flag is visible for permanent
* tables, it is {@code false} for them, and only changed for {@code TemporaryBuffer}s.
*/
protected boolean ignoreBeforeTracking = false;
/** Current persistence context. It is updated in multi-tenant environment, when the tenant changes. */
protected Persistence.Context persistenceContext = null;
/** Cached hash code */
private final int hashCode;
/** Logical database or database alias associated with this buffer */
private final String ldbOrAlias;
/** Invocation handler for DMO proxy associated with this buffer */
private final Handler handler = new Handler();
/**
* Grouping DMO and Buffer interface associated with this buffer.
* Ex: {@code dmo-path.Foo_1$Buf} or {@code dmo-path.Bar_1_1$Buf} for temp-tables.
*/
private final Class<? extends Buffer> dmoBufInterface;
/**
* Interface which defines DMO's API and {@code Table} metadata annotation.
* (Ex: {@code dmo-path.Foo} or {@code dmo-path.Bar_1} for temp-tables).
*/
private final Class<? extends DataModelObject> dmoInterface;
/** The DMO meta information for this buffer. */
private final DmoMeta dmoInfo;
/** Map of {@link DatumAccess} setter instances, for each property. */
private Map<String, DatumAccess> setterDatums = null;
/** Map of {@link DatumAccess} getter instances, for each property. */
private Map<String, DatumAccess> getterDatums = null;
/** Set of getter methods of the current DMO interface */
private final Set<Method> getters;
/** Property changes pending submission to the ChangeBroker */
private Object[][] unreportedChanges = null;
/**
* Helper object to manage database trigger information. DO NOT instantiate it, yet, wait for the buffer
* to be populated and then use the factory to get the associated instance with respective DMO.
*/
private TriggerTracker triggerTracker;
/** Helper object to track unique constraint information for uncommitted transactions */
private final UniqueTracker uniqueTracker;
/** Context-local API for unique constraint tracker */
private final UniqueTracker.Context uniqueTrackerCtx;
/** Helper to use the ProcedureManager without any context local lookups. */
protected final ProcedureManager.ProcedureHelper pm = ProcedureManager.getProcedureHelper();
/**
* Flag indicating if a buffer is exposed to the world until its creator procedure gets
* deleted, in cases when its associated procedure is ran persistent
*/
private boolean worldScope = false;
/** Temporary override of legacy name. */
private String overrideLegacyName = null;
/** The old name of the buffer, after it was overridden by a proxy. */
private String oldVariable = null;
/** Legacy name of this buffer */
private String variable;
/** Legacy name of this buffer and transaction manager's lookup key for this buffer */
private String token;
/** Object which manages dirty read sharing across sessions */
private DirtyShareContext dirtyContext = null;
/** Buffer which created current, unflushed record, if different from this one */
private RecordBuffer creatingBuffer = null;
/**
* The persistent procedure which defined this buffer. This is null if the buffer
* was not defined inside a persistent procedure.
*/
private Object persistentProc = null;
/** Name of (post-conversion) database table associated with this buffer */
private String table = null;
/** Buffer type (key consisting of database, table, and undoable flag) */
private BufferType bufferType = null;
/** Class which defines DMO's implementation */
private final Class<? extends Record> dmoClass;
/** Persistence service object */
private Persistence persistence = null;
/** Logical name of database associated with this buffer */
private String logicalDatabase = null;
/** Foreign association synchronization helper objects */
private ArrayList<AssociationSyncher> assocSynchers = null;
/** Names of properties which represent legacy foreign keys */
private Set<String> legacyForeignKeys = null;
/** Object which identifies modified indexes */
private IndexHelper indexHelper = null;
/** Properties which participate in any indexes of the backing table */
private BitSet indexedProperties = null;
/** Object which tracks locks pinned by this buffer */
private PinnedLocks pinnedLocks = null;
/** Convenience reference to associated DMO proxy */
private BufferReference dmoProxy = null;
/** DMO data record which currently backs this buffer */
private Record currentRecord = null;
/** Latest DMO data record to back this buffer */
private Record snapshot = null;
/** Temporary record stack used for purposes such as sorting */
private List<Record> tempRecords = null;
/** Helpers which null out foreign key references to deleted DMOs */
private List<ForeignNuller> foreignNullers = null;
/** Scope depth at which transient record was created */
private int createScope = -1;
/** Is current record newly created (not read from the database)? */
private boolean newlyCreated = false;
/**
* Marks the {@code currentRecord} as being set from an OUTER query. In this case, the error 91,
* {@code No <buffer> record is available.} is NOT emitted when a field is accessed in read-only mode but
* the buffer is empty. This is somewhat logical because the buffer is expected not to be loaded with a
* record.
*/
private boolean unknownMode = false;
/** Indicates if last find failed because record could not be locked */
private boolean locked = false;
/** Indicates if last find failed because criteria was ambiguous */
private boolean ambiguous = false;
/** Is a transaction commit necessary upon ending a batch assign? */
private boolean commitPending = false;
/** Is buffer currently the destination of a bulk copy operation? */
private boolean bulkCopy = false;
/** Scope at which iterate should not release the current record */
private int iterateReleaseOverrideScope = -1;
/** Sort index last used by a query updating the contents of this buffer */
private SortIndex sortIndex = null;
/** Off-end status of query which last updated this buffer */
private OffEnd offEnd = OffEnd.NONE;
/** Is current record actually a dirty copy from the dirty database? */
private boolean dirtyCopy = false;
/** Flag indicating the rollback is caused by a off-end query */
private boolean offEndRollback = false;
/** Flag indicating that the block to which the buffer is scoped is retrying */
private boolean retrying = false;
/** Flags this record buffer as readonly. Readonly buffers cannot be assigned. */
private boolean readonly = false;
/** Flags this record buffer as a "fake" buffer (e.g., the old buffer in a write trigger) */
private boolean fake = false;
/** This flags that the record held is a VST. */
private final boolean vst;
/** Flag that indicates whether this buffer is backed by a temporary table. */
private final boolean isTemporary;
/**
* Flags whether the record stored has been write-touched. That is, whether a setter was invoked on the
* DMO, regardless whether the value was not actually changed since the last load of the record. Unlike
* {@code recordChanged} that tracks external changes (from other users), this boolean value tracks
* write-touches only from current user.
*/
private boolean isTouched = false;
/**
* Flag used to avoid recursive call while invoking ROW-UPDATE event procedure while another block is
* finished.
*/
private boolean firingRowUpdate = false;
/** Determines if this buffer is dynamic. */
private boolean dynamic = false;
/**
* Marker for a CREATE-ROW callback in process. When this flag is on, the buffer contains a transient
* record but the before image was not yet created for it. In the event of a DELETE operation in a
* subsequent callback, the record is simply discarded instead of creating a superfluous delete
* before-image for it.
*/
protected boolean createRowCallback = false;
/** The query off-end listener associated with this record buffer. */
private final QueryOffEndListener offEndListener = new QueryOffEndListener()
{
public void initialize() { offEndRollback = true; persistenceContext.useSession(); }
public void finish() { offEndRollback = false; offEnd = OffEnd.NONE; persistenceContext.releaseSession(); }
public void reset() { offEnd = OffEnd.FRONT; setCurrentRecord(null, false, false, false, false, false); resetSnapshot(); }
};
/**
* Flag to mark this buffer as changed by another user on a multi-user
* environment. Its value is will be returned by currentChanged() method.
*/
private boolean recordChanged = false;
/**
* Temporary copy of the <code>currentRecord</code> used to detect if its value has changed
* as a result of refreshing from db (FIND CURRENT / GET CURRENT).
* This field is normally null except for armCurrentChanged/updateCurrentChanged bracketing.
*/
private Record shadowCopyRecord = null;
/** Number of fields in the legacy table associated with this buffer (initialized lazily) */
private int numFields = -1;
/** The undoable instance for this buffer. */
private LightweightUndoable undoable = null;
/** The metadata of the record to be stored in this buffer. */
private RecordMeta metadata = null;
/** The data model AST for this buffer. Cached when a dynamic query is used with this buffer. */
private DataModelAst dataModelAst;
/** The table schema AST for this buffer. Cached when a dynamic query is used with this buffer. */
private ProgressAst schemaTableAst;
/** The set of scopes where the buffer was registered with {@link BufferManager byLegacyName}. */
private final RoaringBitmap byLegacyNameScopes = new RoaringBitmap();
/** The set of scopes where the buffer was registered with {@link BufferManager allBuffers}. */
private final RoaringBitmap allBufferScopes = new RoaringBitmap();
/** The set of scopes where the buffer was registered with {@link BufferManager buffersByDMOBufClass}. */
private final RoaringBitmap buffersByDmoClassScopes = new RoaringBitmap();
/** The set of scopes where the buffer was registered with {@link BufferManager loadedBuffers}. */
private final RoaringBitmap loadedBuffersScopes = new RoaringBitmap();
/** The set of scopes where the buffer was registered with {@link BufferManager openBuffers}. */
private final RoaringBitmap openBuffersScopes = new RoaringBitmap();
/** Bi-directional map of legacy field names (lower case) to DMO property names */
private LegacyFieldNameMap legacyFieldNameMap = null;
/** Count the warnings with the intent of reducing the number of log messages. */
private static int unimplementedFastCopyWarningCounter = 0;
/** Cache of the list returned by {@link #recordBuffers()}. */
private List<RecordBuffer> recordBuffers = null;
/** The defining type where this buffer is constructed. */
private final Class<?> definingType;
/** Cache the trigger map from {@link TriggerTracker}. */
private final Map<Object, TriggerTracker> triggerMap;
/** Flag indicating OffEnd.FRONT is reversible to OffEnd.BACK in case of last/prev query*/
private boolean reversibleOffend = false;
/** Flag indicating that buffer is used as a destination in BUFFER-COPY operation for AUTO-SYNCHRONIZE. */
private boolean bufferCopyDestination = false;
/**
* Implicit constructor. Should be used ONLY in the context of a proxy. An invocation handler
* should manage the methods inside this instance.
*/
protected RecordBuffer()
{
dmoBufInterface = null;
hashCode = 0;
getters = null;
dmoInterface = null;
triggerTracker = null;
vst = false;
dmoClass = null;
ldbOrAlias = null;
uniqueTracker = null;
uniqueTrackerCtx = null;
dmoInfo = null;
isTemporary = false;
definingType = null;
triggerMap = TriggerTracker.getTriggerMap();
}
/**
* Constructor. This class is only instantiated via the {@link #define} factory method.
*
* @param def
* The currently executing OE class or external program.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link
* Buffer} methods.
* @param ldbOrAlias
* Logical name of database with which this buffer will be
* associated permanently, or an alias previously created for the
* target database.
* @param variable
* Name of the variable with which the record buffer is associated
* for the purposes of differentiating multiple buffers backed by
* the same DMO type.
*/
protected <T extends Buffer> RecordBuffer(Class<?> def,
Class<T> dmoBufIface,
String ldbOrAlias,
String variable)
{
this(def, dmoBufIface, ldbOrAlias, variable, -1);
}
/**
* Constructor. This class is only instantiated via the {@link #define} factory method.
*
* @param def
* The currently executing OE class or external program.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link Buffer} methods.
* @param ldbOrAlias
* Logical name of database with which this buffer will be associated permanently, or
* an alias previously created for the target database.
* @param variable
* Name of the variable with which the record buffer is associated for the purposes of
* differentiating multiple buffers backed by the same DMO type.
* @param blockDepth
* Zero-based depth of the block (from the outermost scope) at which the buffer is
* created or <code>-1</code> for the current scope.
*/
protected <T extends Buffer> RecordBuffer(Class<?> def,
Class<T> dmoBufIface,
String ldbOrAlias,
String variable,
int blockDepth)
{
triggerMap = TriggerTracker.getTriggerMap();
this.hashCode = super.hashCode();
this.dmoBufInterface = dmoBufIface;
// extract only the DMO schema-specific interface (i.e., exclude the Buffer interface)
Class<? extends DataModelObject> dmoIface = null;
Class<?>[] superIfaces = dmoBufIface.getInterfaces();
int len = superIfaces.length;
for (int i = 0; i < len; i++)
{
Class<?> next = superIfaces[i];
if (DataModelObject.class.isAssignableFrom(next))
{
dmoIface = (Class<? extends DataModelObject>) next;
break;
}
}
if (dmoIface == null)
{
throw new IllegalArgumentException("Invalid DMO interface used to create RecordBuffer");
}
this.ldbOrAlias = ldbOrAlias;
this.variable = variable;
if (variable != null)
{
this.token = BufferManager.get().generateUniqueToken(variable);
}
this.dmoInfo = DmoMetadataManager.registerDmo(dmoIface);
this.dmoInterface = dmoInfo.getAnnotatedInterface();
this.dmoClass = dmoInfo.getImplementationClass();
this.isTemporary = isTemporary();
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, describe() + ": constructing record buffer");
}
// set buffer as read-only if underlying table is read-only
if (DmoMetadataManager.isReadOnly(dmoClass.getName()))
{
this.readonly = true;
}
// store the set of getter methods
getters = PropertyHelper.pojoPropertiesByGetter(this.dmoInterface).keySet();
initPinnedLockTypes();
if (blockDepth == -1)
{
if (def == null)
{
// this can be null only for external programs.
def = pm.getInstantiatingExternalProgramClass();
if (def == null)
{
// we must be executing business logic, and not initiating a program/OE class
Iterator<Class<?>> iter = pm.executingIterator();
if (iter.hasNext())
{
def = iter.next();
}
else
{
throw new IllegalStateException("Could not determine the defining class!");
}
}
}
// the c'tor will be invoked in most cases from the program's default c'tor.
// so, there will be no opened scope in P2J - and the registration with
// the BufferManager will be leaked to the block from the parent program
// which executed this program.
// so, the registration is delayed until the next scope is entered.
// if any exceptions are thrown during initialization, the registration
// must not happen - so this was moved at the end of the c'tor.
bufferManager.registerPending(def, this);
this.definingType = def;
}
else
{
this.definingType = null;
bufferManager.addToAllBuffersArray(blockDepth, this);
}
// get unique tracker for this buffer and unique tracker context for this user session
uniqueTracker = UniqueTracker.getInstance(dmoInfo);
uniqueTrackerCtx = UniqueTracker.getContext();
// check whether this is a VST table
vst = dmoInfo.isMeta() && dmoInfo.metaTable.category == MetadataManager.Category.VST;
}
/**
* Define a new buffer which is based upon the specified interface. The buffer is created
* in read-only mode, temporarily for the scope of a trigger. It is in associated with the
* specified database.
*
* @param <T>
* Record type.
* @param def
* The currently executing OE class or external program.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link Buffer} methods.
* @param database
* Name of the database which will be associated with the new record buffer.
* @param roValue
* The read-only value.
* @param fake
* {@code true} to mark this buffer as a "fake" buffer.
*
* @return An object which implements the specified interface, which has roValue as
* immutable data record.
*
* @see #isFake()
*/
public static <T extends Buffer> T defineReadOnly(Class<?> def,
Class<T> dmoBufIface,
String database,
Record roValue,
boolean fake)
{
if (FwdServerJMX.JMX_DEBUG)
{
return DEF_BUFFER.timerWithReturn(() -> defineReadOnlyImpl(def, dmoBufIface, database, roValue, fake));
}
else
{
return defineReadOnlyImpl(def, dmoBufIface, database, roValue, fake);
}
}
/**
* Define a new buffer which is based upon the specified interface. The
* buffer is permanently associated with the specified database. It will
* be registered with the transaction manager under the specified variable
* name.
*
* @param <T>
* Record type.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link
* Buffer} methods.
* @param database
* Name of the database which will be associated with the new
* record buffer.
* @param variable
* Name of the variable with which the record buffer is associated
* for the purposes of differentiating multiple buffers backed by
* the same DMO type.
*
* @return An object which implements the specified interface, but which
* initially has no backing data record.
*/
public static <T extends Buffer> T define(Class<T> dmoBufIface, String database, String variable)
{
return define(dmoBufIface, database, variable, null);
}
/**
* Define a new buffer which is based upon the specified interface. The
* buffer is permanently associated with the specified database. It will
* be registered with the transaction manager under the specified variable
* name.
*
* @param <T>
* Record type.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link Buffer} methods.
* @param database
* Name of the database which will be associated with the new
* record buffer.
* @param variable
* Name of the variable with which the record buffer is associated
* for the purposes of differentiating multiple buffers backed by
* the same DMO type.
* @param legacyName
* Legacy name of the buffer.
*
* @return An object which implements the specified interface, but which
* initially has no backing data record.
*/
public static <T extends Buffer> T define(Class<T> dmoBufIface,
String database,
String variable,
String legacyName)
{
return define(null, dmoBufIface, database, variable, legacyName, -1);
}
/**
* Define a new buffer which is based upon the specified interface. The
* buffer is permanently associated with the specified database. It will
* be registered with the transaction manager under the specified variable
* name.
*
* @param <T>
* Record type.
* @param def
* The currently executing OE class or external program.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link
* Buffer} methods.
* @param database
* Name of the database which will be associated with the new
* record buffer.
* @param variable
* Name of the variable with which the record buffer is associated
* for the purposes of differentiating multiple buffers backed by
* the same DMO type.
* @param legacyName
* Legacy name of the buffer.
*
* @return An object which implements the specified interface, but which
* initially has no backing data record.
*/
public static <T extends Buffer> T define(Class<?> def,
Class<T> dmoBufIface,
String database,
String variable,
String legacyName)
{
return define(def, dmoBufIface, database, variable, legacyName, -1);
}
/**
* Define a new buffer which is based upon the specified interface. The
* buffer is permanently associated with the specified database. It will
* be registered with the transaction manager under the specified variable
* name.
*
* @param <T>
* Record type.
* @param def
* The currently executing OE class or external program.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link
* Buffer} methods.
* @param database
* Name of the database which will be associated with the new
* record buffer.
* @param variable
* Name of the variable with which the record buffer is associated
* for the purposes of differentiating multiple buffers backed by
* the same DMO type.
* @param legacyName
* Legacy name of the buffer.
* @param blockDepth
* Zero-based depth of the block (starting from the outermost scope) at which the
* buffer is defined or <code>-1</code> for the current scope.
*
* @return An object which implements the specified interface, but which
* initially has no backing data record.
*/
public static <T extends Buffer> T define(Class<?> def,
Class<T> dmoBufIface,
String database,
String variable,
String legacyName,
int blockDepth)
{
if (FwdServerJMX.JMX_DEBUG)
{
Buffer[] res = new Buffer[1];
DEF_BUFFER.timer(() ->
{
RecordBuffer buffer = new RecordBuffer(def, dmoBufIface, database, variable, blockDepth);
res[0] = createProxy(dmoBufIface, buffer, legacyName);
});
return (T) res[0];
}
else
{
RecordBuffer buffer = new RecordBuffer(def, dmoBufIface, database, variable, blockDepth);
return createProxy(dmoBufIface, buffer, legacyName);
}
}
/**
* Create a proxy for the given buffer instance, to act as an instance of the specified type, so it can be
* passed as an argument to a direct Java method call.
*
* @param ref
* The buffer instance which needs to be proxied.
* @param type
* The DMO type to which the reference will be proxied.
*
* @return A proxy to access the given reference as the given type.
*/
public static <T extends Buffer> T bufferProxy(Buffer ref, Class<T> type)
{
return (T) TemporaryBuffer.mutableBufferProxy(ref, type);
}
/**
* When a buffer is passed as a parameter, a new instance should be created in order to
* differentiate the references. Even if the buffer parameter is INPUT-OUTPUT, the DMO alias
* and buffer name differ based on the reference used in the conversion. Thus, this will
* return a proxy to the buffer in order to deliberately override the DMO alias or buffer name
* depending on the case. The proxy returned is a dummy {@link RecordBuffer}; any access to this
* buffer should be done using the handler, otherwise inconsistent data will be returned.
*
* @param <T>
* Record type.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link Buffer} methods.
* @param aliasOf
* The buffer to be aliased.
* @param varName
* The converted variable name.
* @param legacyName
* The legacy variable name.
*
* @return A proxy for the {@code aliasOf} buffer.
*/
public static <T extends Buffer> T defineAlias(Class<T> dmoBufIface,
Buffer aliasOf,
String varName,
String legacyName)
{
return makeArgumentBuffer(dmoBufIface, aliasOf, varName, legacyName);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
* The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer. <code>null</code> if
* the unnamed pool must be used.
*/
public static void create(handle h, String table, String bufName, String widgetPool)
{
create(h,
new character(table),
new character(bufName),
(widgetPool == null ? null : new character(widgetPool)));
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
* The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer. <code>null</code> if
* the unnamed pool must be used.
*/
public static void create(handle h, String table, String bufName, character widgetPool)
{
create(h, new character(table), new character(bufName), widgetPool);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
* The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer. <code>null</code> if
* the unnamed pool must be used.
*/
public static void create(handle h, String table, character bufName, String widgetPool)
{
create(h,
new character(table),
bufName,
(widgetPool == null ? null : new character(widgetPool)));
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
* The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer. <code>null</code> if
* the unnamed pool must be used.
*/
public static void create(handle h, String table, character bufName, character widgetPool)
{
create(h, new character(table), bufName, widgetPool);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
* The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer. <code>null</code> if
* the unnamed pool must be used.
*/
public static void create(handle h, character table, String bufName, String widgetPool)
{
create(h,
table,
new character(bufName),
(widgetPool == null ? null : new character(widgetPool)));
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
* The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, character table, String bufName, character widgetPool)
{
RecordBuffer.create(h, table, new character(bufName), widgetPool);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
* The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer. <code>null</code> if
* the unnamed pool must be used.
*/
public static void create(handle h, character table, character bufName, String widgetPool)
{
create(h, table, bufName, (widgetPool == null ? null : new character(widgetPool)));
}
/**
* Dynamically creates a new buffer for a meta table and assign it to handle h.
* Note: this must only be used for _meta buffers.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param p
* The {@code Persistence} object.
* @param tableName
* The meta name for the .
*/
public static void createMeta(handle h, Persistence p, String tableName, String ldbName)
{
if (p == null || !p.getDatabase().isMeta())
{
return; // no-op
}
Class<? extends Record> meta = TableMapper.getDMOClass("_meta", tableName);
if (meta == null)
{
// some tables are store in the primary permanent database (_user / _sec-authentication-*)
meta = TableMapper.getDMOClass(ldbName, tableName);
if (meta == null)
{
// sorry, could not get the table from TableMapper
return;
}
else
{
// switch to this new persistence object
p = PersistenceFactory.getInstance(ldbName);
}
}
DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(meta);
DataModelObject dmo = createDynamicBufferForPermTable(
dmoInfo.getAnnotatedInterface(), tableName, ldbName);
RecordBuffer rb = ((BufferImpl) dmo).buffer();
rb.initialize(false, p, p.getDatabase());
rb.logicalDatabase = ldbName;
rb.readonly = false; // allow to write when metadata is processed by admin tools
h.assign(dmo);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of {@code CREATE-BUFFER} statement that uses the table-name.
* The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer. {@code null} if it is not explicitly specified.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer. {@code null} if
* the unnamed pool must be used.
*/
public static void create(handle h, character table, character bufName, character widgetPool)
{
if (!WidgetPool.validWidgetPool(widgetPool))
{
return;
}
if (table.isUnknown() || (bufName != null && bufName.isUnknown()))
{
ErrorManager.recordOrThrowError(
7333,
"FOR TABLE phrase or CONNECT phrase in CREATE widget could not be evaluated",
false);
return;
}
String tableName = table.getValue();
if (tableName.isEmpty())
{
ErrorManager.recordOrThrowError(
7334,
String.format("Could not create buffer object for table %s", tableName),
false);
return;
}
Persistence persistence = DynamicTablesHelper.findExistingTable(tableName);
if (persistence == null)
{
ErrorManager.recordOrThrowError(
7334,
String.format("Could not create buffer object for table %s", tableName),
false);
return;
}
String[] parts = DynamicTablesHelper.parseTableName(tableName);
tableName = parts[0];
String actualBufName = (bufName != null) ? bufName.getValue() : tableName;
DataModelObject dmo;
if (!persistence.isTemporary())
{
Database database = persistence.getDatabase(Persistence.PRIVATE_CTX);
String schema = DatabaseManager.getSchema(database);
Class<? extends Record> dmoClass = TableMapper.getDMOClass(schema, tableName);
DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoClass);
String ldbName = ConnectionManager.get().getLDBName(database);
dmo = createDynamicBufferForPermTable(dmoInfo.getAnnotatedInterface(), actualBufName, ldbName);
}
else
{
tableName = TextOps.rightTrimLower(tableName);
BufferManager bufferManager = BufferManager.get();
int blockDepth = bufferManager.getStaticTempTableDepth(tableName);
if (blockDepth == -1)
{
throw new IllegalStateException("Cannot locate static temp table " + tableName);
}
StaticTempTable staticTempTable = bufferManager.getStaticTempTable(null, tableName);
TemporaryBuffer master = staticTempTable.defaultBuffer();
dmo = TemporaryBuffer.createDynamicBufferForTempTable(actualBufName, master);
}
RecordBuffer rb = get(dmo);
if (!rb.isDynamic())
{
// most likely this will never happen as in both cases, dmo is already configured as
// dynamic in the createDynamicBuffer..()
rb.setDynamic();
((BufferImpl) dmo).interlink(); // HandleChain implementation
}
h.assign(dmo);
((BufferImpl) dmo).addToPool(widgetPool);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
*/
public static void create(handle h, String table, String bufName)
{
create(h, new character(table), new character(bufName), (character) null);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
*/
public static void create(handle h, String table, character bufName)
{
create(h, new character(table), bufName, (character) null);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
*/
public static void create(handle h, character table, String bufName)
{
create(h, table, new character(bufName), (character) null);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
* @param bufName
* The name for the buffer.
*/
public static void create(handle h, character table, character bufName)
{
create(h, table, bufName, (character) null);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
*/
public static void create(handle h, String table)
{
create(h, new character(table), (character) null, (character) null);
}
/**
* Creates a dynamic buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param table
* The name of the table to create the buffer for.
*/
public static void create(handle h, character table)
{
create(h, table, (character) null, (character) null);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
* a temporary table reference.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param tt
* The temporary table to create the buffer for.
*/
public static void create(handle h, Temporary tt)
{
create(h, tt, (character) null);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
* a temporary table reference.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param tt
* The temporary table to create the buffer for.
* @param bufName
* The name for the buffer.
*/
public static void create(handle h, Temporary tt, String bufName)
{
create(h, tt, bufName, (character) null);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
* a temporary table reference.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param tt
* The temporary table to create the buffer for.
* @param bufName
* The name for the buffer.
*/
public static void create(handle h, Temporary tt, character bufName)
{
create(h, tt, bufName, "");
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
* a temporary table reference. The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param tt
* The temporary table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, Temporary tt, String bufName, String widgetPool)
{
create(h,
tt,
new character(bufName),
(widgetPool == null ? null : new character(widgetPool)));
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
* a temporary table reference. The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param tt
* The temporary table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, Temporary tt, String bufName, character widgetPool)
{
create(h, tt, new character(bufName), widgetPool);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
* a temporary table reference. The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param tt
* The temporary table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, Temporary tt, character bufName, String widgetPool)
{
create(h, tt, bufName, (widgetPool == null ? null : new character(widgetPool)));
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
* a temporary table reference. The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param tt
* The temporary table to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, Temporary tt, character bufName, character widgetPool)
{
handle hsrc = new handle();
// this needs the resource, not the RecordBuffer instance
hsrc.assign((Buffer) RecordBuffer.get(tt).getDMOProxy());
create(h, hsrc, bufName, widgetPool);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
* buffer already assigned to a handle.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param hsrc
* The handle to table or other table buffer to create the buffer for.
*/
public static void create(handle h, handle hsrc)
{
create(h, hsrc, (character) null, (character) null);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
* buffer already assigned to a handle.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param hsrc
* The handle to table or other table buffer to create the buffer for.
* @param bufName
* The name for the buffer.
*/
public static void create(handle h, handle hsrc, String bufName)
{
create(h, hsrc, bufName, (character) null);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
* buffer already assigned to a handle.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param hsrc
* The handle to table or other table buffer to create the buffer for.
* @param bufName
* The name for the buffer.
*/
public static void create(handle h, handle hsrc, character bufName)
{
create(h, hsrc, bufName, (character) null);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
* buffer already assigned to a handle. The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param hsrc
* The handle to table or other table buffer to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, handle hsrc, String bufName, String widgetPool)
{
create(h,
hsrc,
new character(bufName),
(widgetPool == null ? null : new character(widgetPool)));
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
* buffer already assigned to a handle. The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param hsrc
* The handle to table or other table buffer to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, handle hsrc, String bufName, character widgetPool)
{
create(h, hsrc, new character(bufName), widgetPool);
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
* buffer already assigned to a handle. The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param hsrc
* The handle to table or other table buffer to create the buffer for.
* @param bufName
* The name for the buffer.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, handle hsrc, character bufName, String widgetPool)
{
create(h, hsrc, bufName, (widgetPool == null ? null : new character(widgetPool)));
}
/**
* Dynamically creates a new buffer and assign it to handle h.
* This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
* buffer already assigned to a handle. The new buffer will be created in the specified widget pool.
*
* @param h
* The handle to which the created buffer is assigned to.
* @param hsrc
* The handle to table or other table buffer to create the buffer for.
* @param bufName
* The name for the buffer or <code>null</code> if it is not explicitly specified.
* @param widgetPool
* The name of the widget pool that contains the dynamic buffer.
*/
public static void create(handle h, handle hsrc, character bufName, character widgetPool)
{
if (!WidgetPool.validWidgetPool(widgetPool))
{
return;
}
if (hsrc.isUnknown() || (bufName != null && bufName.isUnknown()))
{
ErrorManager.recordOrThrowError(
7333,
"FOR TABLE phrase or CONNECT phrase in CREATE widget could not be evaluated",
false);
return;
}
AbstractTempTable srcTable = null;
RecordBuffer srcBuffer = null;
DataModelObject dmo;
Object src = hsrc.get();
if (TempTable.class.isAssignableFrom(src.getClass()))
{
srcTable = (AbstractTempTable) src;
}
else if (DataModelObject.class.isAssignableFrom(src.getClass()))
{
srcBuffer = RecordBuffer.get((DataModelObject) src);
srcTable = srcBuffer.getParentTable();
}
else
{
// unknown handle type
String name = bufName != null ? bufName.getValue() : "";
ErrorManager.recordOrThrowError(7334, "Could not create buffer object for table " + name, false);
return;
}
if (srcTable != null)
{
// temporary buffer
int blockDepth;
TemporaryBuffer master;
if (srcTable._dynamic())
{
TempTableBuilder ttb = (TempTableBuilder) srcTable;
blockDepth = 0;
master = (TemporaryBuffer) RecordBuffer.get((Temporary) ttb.defaultBufferHandleNative());
}
else
{
BufferManager bufMan = srcBuffer != null ? srcBuffer.bufferManager : BufferManager.get();
StaticTempTable staticTempTable = (StaticTempTable) srcTable;
blockDepth = bufMan.getStaticTempTableDepth(staticTempTable);
master = staticTempTable.defaultBuffer();
}
if (master == null || blockDepth == -1)
{
throw new IllegalStateException();
}
String actualBufName = bufName != null ? bufName.getValue() : srcTable._name();
dmo = TemporaryBuffer.createDynamicBufferForTempTable(actualBufName, master);
}
else
{
// permanent buffer
String actualBufName = bufName != null ? bufName.getValue() : srcBuffer.getDmoInfo().legacyTable;
dmo = createDynamicBufferForPermTable(srcBuffer.getDMOInterface(),
actualBufName,
srcBuffer.getLogicalDatabase());
}
RecordBuffer rb = RecordBuffer.get(dmo);
if (!rb.isDynamic())
{
// most likely this will never happen as in both cases, dmo is already configured as
// dynamic in the createDynamicBuffer..()
rb.setDynamic();
((BufferImpl) dmo).interlink(); // HandleChain implementation
}
h.assign(dmo);
((BufferImpl) dmo).addToPool(widgetPool);
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>character</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static character recordNotAvailableCharacter(String buffer)
{
errorNotAvailable(buffer);
return new character();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>comhandle</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static comhandle recordNotAvailableComhandle(String buffer)
{
errorNotAvailable(buffer);
return new comhandle();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>date</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static date recordNotAvailableDate(String buffer)
{
errorNotAvailable(buffer);
return new date();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>datetime</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static datetime recordNotAvailableDatetime(String buffer)
{
errorNotAvailable(buffer);
return new datetime();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>datetimetz</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static datetimetz recordNotAvailableDatetimetz(String buffer)
{
errorNotAvailable(buffer);
return new datetimetz();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>decimal</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static decimal recordNotAvailableDecimal(String buffer)
{
errorNotAvailable(buffer);
return new decimal();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>handle</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static handle recordNotAvailableHandle(String buffer)
{
errorNotAvailable(buffer);
return new handle();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>integer</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static integer recordNotAvailableInteger(String buffer)
{
errorNotAvailable(buffer);
return new integer();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>int64</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static int64 recordNotAvailableInt64(String buffer)
{
errorNotAvailable(buffer);
return new int64();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>logical</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static logical recordNotAvailableLogical(String buffer)
{
errorNotAvailable(buffer);
return new logical();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>raw</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static raw recordNotAvailableRaw(String buffer)
{
errorNotAvailable(buffer);
return new raw();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>recid</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static recid recordNotAvailableRecid(String buffer)
{
errorNotAvailable(buffer);
return new recid();
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @return An unknown value of the <code>rowid</code> type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
public static rowid recordNotAvailableRowid(String buffer)
{
errorNotAvailable(buffer);
return new rowid();
}
/**
* Open a new buffer scope for the record buffer which backs the specified
* DMO. This ensures that the {@link BufferManager} is tracking this
* buffer for block scope entries and exits within transactions.
*
* @param dmos
* Variable length list of DMO instances returned by previous
* calls to {@link #define}.
*/
public static void openScope(DataModelObject... dmos)
{
for (DataModelObject dmo : dmos)
{
get(dmo).openScope();
}
}
/**
* Open a new buffer scope at the specified block depth for the record buffer which backs
* the specified DMO. This ensures that the {@link BufferManager} is tracking this buffer for
* block scope entries and exits within transactions.
*
* @param blockDepth
* Zero-based depth of the block (starting from the outermost block) at which the
* buffer scopes are opened.
* @param dmos
* Variable length list of DMO instances returned by previous
* calls to {@link #define}.
*/
public static void openScopeAt(int blockDepth, DataModelObject... dmos)
{
for (DataModelObject dmo : dmos)
{
get(dmo).openScopeAt(blockDepth);
}
}
/**
* Worker method that executes a given block of code in batch assign mode, by bracketing the call in
* {@link #startBatch} and {@link #endBatch} calls.
*
* @param code
* The block of code to execute in batch assign mode.
*
* @see #startBatch
* @see #endBatch
*/
public static void batch(Runnable code)
{
BufferManager bufMan = BufferManager.get();
startBatch(bufMan, false);
boolean batchError = true;
try
{
code.run();
batchError = false;
}
finally
{
endBatch(bufMan, batchError);
}
}
/**
* Worker method that executes a given block of code in batch assign mode, having the silent
* error mode enabled.
*
* @param code
* The block of code to execute in batch assign mode.
*
* @return The value of the ERROR-STATUS:ERROR flag when the execution is complete.
*
* @see #batch(Runnable)
* @see ErrorManager#silent(Runnable)
*/
public static boolean silentBatch(Runnable code)
{
return ErrorManager.silent(() -> batch(code));
}
/**
* Enter batch assign mode in the current context. Batch assign mode allows multiple DMO property
* assignments without incurring validation. Validation for all modified buffers in the current context is
* performed when batch mode is ended for the current context.
*
* @param bufMan
* The {@link BufferManager} instance.
* @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 #endBatch
*
* @throws IllegalStateException
* if batch mode already is active in the current block or was started but never
* ended in a previous block.
*/
public static void startBatch(BufferManager bufMan, boolean internal)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, Utils.describeContext() + ": entering batch assign mode");
}
bufMan.startBatchAssignMode(internal);
}
/**
* Exit batch assign mode in the current context, triggering pending
* validation on all buffers which were modified while in this mode.
* <p>
* Implementation Note: as a simplification, dirty buffers are validated
* in their entirety, not only the fields that were modified during the
* most recent batch. However, this should be safe (though possibly less
* efficient), since the buffers must already have been valid upon
* entering batch mode.
*
* @param bufMan
* The {@link BufferManager} instance.
* @param error
* {@code false} all assignments end without issues. Validate them.
* <p>
* {@code true} if some error occurred during the batch. In this case all assignments performed
* in the batch block MUST be reverted!<br>
* <strong>TODO: implement this case!</strong>
*
* @return The number of errors (usually buffers which failed validation).
* If there is none, 0 is returned.
*
* @see #startBatch
*
* @throws IllegalStateException
* if the current context is not in batch mode when this method is invoked.
* @throws ErrorConditionException
* if validation fails on any buffer which was modified in batch mode;
* if there is any error accessing the database.
*/
public static int endBatch(BufferManager bufMan, boolean error)
{
Map<RecordBuffer, Runnable> bufs = bufMan.endBatchAssignMode();
int fails = 0;
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
"%s: exiting batch assign mode; dirty buffers: %s",
Utils.describeContext(),
bufs == null ? "none" : bufs.toString());
}
if (bufs == null)
{
throw new IllegalStateException("Cannot end batch assign mode; not currently in batch mode");
}
if (error)
{
// TODO: implement the rollback of all fields, attributes and variables altered in this batch
cleanupBatchMode(bufs);
return 0; // no errors occurred during this method
}
try
{
Iterator<RecordBuffer> iter = bufs.keySet().iterator();
while (iter.hasNext())
{
RecordBuffer buffer = iter.next();
boolean commitPending = buffer.isCommitPending();
Persistence persistence = buffer.getPersistence();
boolean exception = false;
try
{
// in batch assign mode, standard validation happens before assign triggers are fired
Record dmo = buffer.getCurrentRecord();
buffer.validateMaybeFlush(dmo, false, false);
// fire assign triggers, if any
TriggerTracker tracker = buffer.getTriggerTracker();
LinkedList<Pair<String, BaseDataType>> triggerFields =
tracker == null ? null : tracker.getPendingBatchFields();
if (triggerFields != null)
{
Class<? extends Buffer> bufIface = buffer.getDMOBufInterface();
Buffer dmoProxy = (Buffer) buffer.getDMOProxy();
String ldb = ((BufferImpl) dmoProxy).buffer().logicalDatabase;
try
{
for (Pair<String, BaseDataType> pair : triggerFields)
{
String prop = pair.getLeft();
if (tracker.isFieldInTrigger(prop))
{
// the field is allowed to be changed inside its trigger, but indirect
// recursion is an error
if (!tracker.isFieldCurrentTrigger(prop))
{
DatabaseTriggerManager.handleError(
1003,
DatabaseEventType.ASSIGN,
buffer.getDmoInfo().legacyTable,
TableMapper.getLegacyFieldName(buffer, prop));
}
continue;
}
// fire the trigger
tracker.pushField(prop);
DatabaseTriggerManager.triggerAssign(bufIface, dmoProxy, prop, pair.getRight(), ldb);
String top = tracker.popField();
assert prop.equals(top); // the property must be the last one pushed
}
}
finally
{
// do the cleanup. The affected properties and their old values can be forgotten now
tracker.clearPendingBatchFields();
}
}
// buffer.syncForeignRelations(false);
buffer.processChange();
}
catch (ValidationException exc)
{
buffer.clearUnreportedChanges();
exception = true;
reportValidationException(exc, buffer, false);
}
catch (PersistenceException exc)
{
exception = true;
throw new RuntimeException(exc);
}
catch (ConditionException exc)
{
// ConditionException may occur into database triggers
buffer.clearUnreportedChanges();
exception = true;
throw exc;
}
finally
{
if (exception)
{
fails++;
buffer.setBufferCopyDestination(false);
}
if (!exception && buffer.bufferCopyDestination)
{
Buffer buf = (Buffer) buffer.getDMOProxy();
if (buf.autoSynchronize().booleanValue())
{
buf.querySynchronize();
}
buffer.setBufferCopyDestination(false);
}
if (commitPending)
{
buffer.setCommitPending(false);
try
{
if (!exception)
{
persistence.commit(!buffer.dmoInfo.multiTenant);
}
else
{
persistence.rollback(!buffer.dmoInfo.multiTenant);
}
}
catch (PersistenceException exc)
{
throw new RuntimeException(exc);
}
}
}
}
}
finally
{
cleanupBatchMode(bufs);
}
return fails;
}
/**
* Get code page of the specified CLOB field.
*
* @param field
* field reference.
*
* @return code page of the specified CLOB field.
*/
public static character getCodePage(FieldReference field)
{
RecordBuffer buf = field.getParentBuffer();
return new character(buf.getCodePage(field.getProperty()));
}
/**
* Get code page of the specified CLOB field.
*
* @param field
* field reference.
*
* @return code page of the specified CLOB field.
*/
public static character getCodePage(BufferField field)
{
BufferFieldImpl impl = (BufferFieldImpl) field;
RecordBuffer buf = RecordBuffer.get((DataModelObject) impl.getDMOProxy());
return new character(buf.getCodePage(impl.getProperty()));
}
/**
* Get the backing, DMO and {@link Buffer} grouping interface for the given object. If the
* object is not a {@link BufferReference} or is null, return null.
*
* @param obj
* The assumed DMO for which the interface is needed.
*
* @return The DMO's interface.
*/
public static Class<?> getDMOBufInterfaceForObject(Object obj)
{
if (!(obj instanceof BufferReference))
{
return null;
}
return ((BufferReference) obj).buffer().getDMOBufInterface();
}
/**
* Getter for doNotProxy array;
* @return The doNotProxy array.
*/
public static Method[] getDoNotProxy()
{
return doNotProxy;
}
/**
* Create a proxy object which client code will use to interact with the backing DMO. Create an
* invocation handler for the proxy which intercepts method requests made on the proxy and
* redirects them as necessary.
*
* @param expectedType
* Interface which defines DMO's public API methods.
* @param argument
* Used to create a proxy based on the expected type.
*
* @return An object which represents the proxy created from the expectedType to the argument's instance.
*/
public static Object createProxy(Class<?> expectedType, Object argument)
{
Class<?>[] ifaces = Temporary.class.isAssignableFrom(expectedType)
? new Class<?>[] { expectedType, BufferReference.class, TempTableRecord.class }
: new Class<?>[] { expectedType, BufferReference.class };
return ProxyFactory.getProxy(BufferImpl.class,
true,
ifaces,
false,
doNotProxyRes,
() ->
{
Class<?> dmoBufIface = expectedType;
Class<? extends DataModelObject> dmoIface = null;
for (int i = 0; i < dmoBufIface.getInterfaces().length; i++)
{
Class<?> iface = dmoBufIface.getInterfaces()[i];
if (DataModelObject.class.isAssignableFrom(iface) &&
!Buffer.class.isAssignableFrom(iface))
{
dmoIface = (Class<? extends DataModelObject>) iface;
break;
}
}
return new DmoProxyPlugin(dmoBufIface, dmoIface);
},
new InvocationHandler()
{
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
Method marg = argument.getClass()
.getMethod(method.getName(),
method.getParameterTypes());
return marg.invoke(argument, args);
}
});
}
/**
* Create a proxy object which client code will use to interact with the backing DMO. Create an
* invocation handler for the proxy which intercepts method requests made on the proxy and
* redirects them as necessary.
*
* @param <T>
* Record type.
* @param dmoBufIface
* Interface which defines DMO's public API methods.
* @param buffer
* Record buffer instance associated with the created proxy.
* @param legacyName
* Legacy name of the buffer.
*
* @return An object which implements the specified interface, but which initially has no
* backing data record.
*/
protected static <T> T createProxy(Class<?> dmoBufIface, RecordBuffer buffer, String legacyName)
{
Class<?> dmoClass = buffer.getDMOClass();
Class<?>[] interfaces = TempRecord.class.isAssignableFrom(dmoClass)
? new Class[] { dmoBufIface, BufferReference.class, TempTableRecord.class }
: new Class[] { dmoBufIface, BufferReference.class };
T dmoProxy = ProxyFactory.getProxy(BufferImpl.class,
interfaces,
false,
doNotProxy,
() -> new DmoProxyPlugin(dmoBufIface, buffer.getDMOInterface()),
buffer.getHandler());
buffer.setDMOProxy((BufferReference) dmoProxy);
((BufferImpl) dmoProxy).doSetName(legacyName == null ? buffer.getDmoInfo().legacyTable : legacyName);
buffer.pm.processResource((WrappedResource) dmoProxy, false);
return dmoProxy;
}
/**
* Given two sets of legacy field names, an additional list of legacy field names (optional),
* and a flag indicating whether the additional list is inclusive or exclusive, find the
* appropriate intersection of field names.
* <p>
* The resulting set will contain at most the intersection of {@code set1} and {@code set2}.
* However, this set may be further restricted if a non-{@code null} {@code fieldList} is
* provided, as follows:
* <ul>
* <li>if {@code exclusive} is {@code true}, all elements of {@code fieldList} are excluded
* from the intersection of {@code set1} and {@code set2}.
* <li>if {@code exclusive} is {@code false}, only those elements present in {@code set1}
* and in {@code set2} and in {@code fieldList} are included in the final result.
* </ul>
*
* @param set1
* First set of legacy field names.
* @param set2
* Second set of legacy field names.
* @param fieldList
* An optional array of legacy field names; may be {@code null}.
* @param exclusive
* if {@code true}, {@code fieldList} is an exclusive list of field names;
* if {@code false}, {@code propList} is an inclusive list. Ignored if {@code
* fieldList} is {@code null}.
*
* @return The set of property names resulting from following the rules
* described above.
*/
protected static Set<String> getCommonFields(Set<String> set1,
Set<String> set2,
String[] fieldList,
boolean exclusive)
{
Set<String> props = null;
// Do we have an explicit property list?
if (fieldList != null)
{
Set<String> explicit = new HashSet<>(Arrays.asList(fieldList));
if (exclusive)
{
// Find intersection of property sets.
props = new HashSet<>(set1);
props.retainAll(set2);
// Exclude properties explicitly listed.
props.removeAll(explicit);
}
else
{
// Include only properties explicitly listed.
props = explicit;
// Find intersection with property sets.
props.retainAll(set1);
props.retainAll(set2);
}
}
else
{
// Find simple intersection of property sets.
props = new HashSet<>(set1);
props.retainAll(set2);
}
return props;
}
/**
* Perform a bulk copy of data from a source record to a destination
* record. Data is copied for the properties listed in <code>propsMap</code>.
* Other properties of the source and destination DMOs are ignored.
*
* @param srcDMO
* Source DMO record (an actual record, not a proxy)
* @param dstDMO
* Destination DMO, which may be a dynamic proxy or the backing
* data model object. A proxy should be used in cases where it
* is necessary to take advantage of flushing/validating logic
* implemented in the proxy invocation handler. The backing DMO
* instance should be used if a raw copy is all that is needed.
* In the latter case, calling code is responsible for creating
* the record, optionally validating it, and persisting it to the
* database at the appropriate time.
* @param propsMap
* Map of property access info objects in source record buffer to those in destination
* record buffer
* @param pkProps
* When not null, a list of destination properties which need to be set to the source
* DMO PK.
*
* @return <code>true</code> if copy is successful, otherwise <code>false</code>.
*
* @throws IllegalAccessException
* if a getter or setter method cannot be invoked because it is not accessible.
* @throws InvocationTargetException
* if an invoked getter or setter method throws an exception.
*/
protected static logical copy(DataModelObject srcDMO,
DataModelObject dstDMO,
Map<DatumAccess, DatumAccess> propsMap,
List<DatumAccess> pkProps)
throws IllegalAccessException,
InvocationTargetException
{
/* TODO: update _UserTableStat counter(s) */
Record srcRec = srcDMO instanceof BufferReference
? ((BufferReference) srcDMO).buffer().getCurrentRecord()
: (Record) srcDMO;
Record dstRec;
RecordBuffer dstRB = null;
boolean hasBefore = false;
if (dstDMO instanceof BufferReference)
{
dstRB = ((BufferReference) dstDMO).buffer();
dstRec = dstRB.getCurrentRecord();
if (dstRec == null)
{
// create it, if it does not exist
dstRB.create();
dstRec = dstRB.getCurrentRecord();
}
hasBefore = ((BufferImpl) dstDMO).isAfterBuffer() && dstRB.getParentTable()._isTrackingChanges();
}
else
{
// Note: if the destination is just a simple [Record] object, there is no buffer accessible to arm
// the [Write] trigger for it ([dstRB] remains null)
dstRec = (Record) dstDMO;
if (dstRec == null)
{
// no destination ?
return new logical(false);
}
}
for (Map.Entry<DatumAccess, DatumAccess> access : propsMap.entrySet())
{
DatumAccess srcAccess = access.getKey();
DatumAccess dstAccess = access.getValue();
Method getter = srcAccess.getAccessor();
Method setter = dstAccess.getAccessor();
Integer extent = srcAccess.getExtent();
PropertyMeta srcProp = srcAccess.getPropertyMeta();
PropertyMeta dstProp = dstAccess.getPropertyMeta();
// if the destination has before table, the before-image must be updated
boolean directAccess = !hasBefore && dstAccess.isDirectAccess(srcAccess);
Object result = null;
try
{
if (extent == null)
{
// invoke usual getter
result = directAccess ? OrmUtils.getField(srcRec, srcProp.getOffset())
: Utils.invoke(getter, srcDMO);
}
else
{
Integer srcExtentIndex = srcAccess.getExtentIndex();
if (srcExtentIndex != null)
{
// invoke indexed getter
result = directAccess ? OrmUtils.getField(srcRec, srcProp.getOffset() + srcExtentIndex)
: Utils.invoke(getter, srcDMO, srcExtentIndex);
}
}
if (extent == null || result != null)
{
Integer dstExtentIndex = dstAccess.getExtentIndex();
if (dstExtentIndex == null)
{
// invoke usual setter
if (directAccess)
{
armWriteTrigger(dstRB, false);
OrmUtils.setField(dstRec, dstProp.getOffset(), result);
}
else
{
Utils.invoke(setter, dstDMO, result);
}
}
else
{
// invoke indexed setter
if (directAccess)
{
armWriteTrigger(dstRB, false);
OrmUtils.setField(dstRec, dstProp.getOffset() + dstExtentIndex, result);
}
else
{
Utils.invoke(setter, dstDMO, dstExtentIndex, result);
}
}
}
else
{
// invoke all pairs of indexed getters and setters; assume extent sizes match
for (int i = 0; i < extent; i++)
{
result = directAccess ? OrmUtils.getField(srcRec, srcProp.getOffset() + i)
: Utils.invoke(getter, srcDMO, i);
if (directAccess)
{
armWriteTrigger(dstRB, false);
OrmUtils.setField(dstRec, dstProp.getOffset() + i, result);
}
else
{
Utils.invoke(setter, dstDMO, i, result);
}
}
}
}
catch (ErrorConditionException e)
{
if (e.getProgressErrorCode() == 15747)
{
//Couldn't update field of target in a BUFFER-COPY statement
ErrorManager.recordOrShowError(5368, dstAccess.propertyMeta.getLegacyName());
return logical.FALSE;
}
else
{
throw e;
}
}
}
if (pkProps != null)
{
rowid srcPk = new rowid(srcRec.primaryKey());
for (DatumAccess da : pkProps)
{
try
{
Method setter = da.getAccessor();
Utils.invoke(setter, dstDMO, srcPk);
}
catch (ErrorConditionException e)
{
if (e.getProgressErrorCode() == 15747)
{
//Couldn't update field of target in a BUFFER-COPY statement
ErrorManager.recordOrShowError(5368, da.propertyMeta.getLegacyName());
return logical.FALSE;
}
else
{
throw e;
}
}
}
}
return logical.TRUE;
}
/**
* Creates simple properties map with mapping properties with the same legacy names. The
* {@code srcLegacyFieldsMap} and {@code dstLegacyFieldsMap} are used to obtain the properties
* names and {@code srcGetters} and {@code dstSetters} are used to check if the types of
* properties have the same type (are directly assignable).
*
* @param legacyFields
* The list of fields to be mapped (legacy fields names).
* @param srcBuf
* The source buffer.
* @param dstBuf
* The destination buffer.
* @param srcGetters
* Map of source DMO methods.
* @param dstSetters
* Map of destination DMO methods.
* @param srcExtents
* Map of source extent properties.
* @param dstExtents
* Map of destination extent properties.
* @param srcLegacyFieldsMap
* Map of legacy field names to properties for source buffer.
* @param dstLegacyFieldsMap
* Map of legacy field names to properties for destination buffer.
* @param srcProps
* Map of source accessor methods to the property meta.
* @param dstProps
* Map of destination accessor methods to the property meta.
* @param srcDatums
* Map of source properties to their {@link DatumAccess} instance.
* @param dstDatums
* Map of destination properties to their {@link DatumAccess} instance.
* @param noLobs
* If {@code true}, exclude any BLOB/CLOB fields from the operation, else include
* these fields.
* @param operationType
* Operation type (COMPARE/COPY).
*
* @return properties map with mapping property to the property with the same name of the
* legacy fields.
*/
protected static Map<DatumAccess, DatumAccess> createSimplePropsMap(Collection<String> legacyFields,
RecordBuffer srcBuf,
RecordBuffer dstBuf,
Map<String, Method> srcGetters,
Map<String, Method> dstSetters,
Map<String, Integer> srcExtents,
Map<String, Integer> dstExtents,
Map<String, String> srcLegacyFieldsMap,
Map<String, String> dstLegacyFieldsMap,
Map<Method, PropertyMeta> srcProps,
Map<Method, PropertyMeta> dstProps,
Map<String, DatumAccess> srcDatums,
Map<String, DatumAccess> dstDatums,
boolean noLobs,
OperationType operationType)
{
Map<DatumAccess, DatumAccess> copyMap = new HashMap<>();
for (String lField : legacyFields)
{
String srcProp = srcLegacyFieldsMap.get(lField);
String dstProp = dstLegacyFieldsMap.get(lField);
DatumAccess srcDatum = srcDatums.get(srcProp);
DatumAccess dstDatum = dstDatums.get(dstProp);
Method srcGetter = srcDatum == null ? srcGetters.get(srcProp) : srcDatum.getAccessor();
Method dstSetter = dstDatum == null ? dstSetters.get(dstProp) : dstDatum.getAccessor();
if (srcGetter == null || dstSetter == null)
{
LOG.log(Level.SEVERE,
"Cannot copy '" + srcBuf.getDMOName() + "." + srcProp + "' to '" +
dstBuf.getDMOName() + "." + dstProp + "'.\n" +
"Getter is " + srcGetter + "\n" +
"Setter is " + dstSetter);
continue;
}
if (isAssignableTypes(srcGetter, dstSetter, noLobs, operationType))
{
if (srcDatum == null)
{
PropertyMeta srcMeta = srcProps.get(srcGetter);
srcDatum = new DatumAccess(srcProp,
null,
srcGetter,
srcExtents.get(srcProp),
srcMeta,
srcBuf.isPropertyIndexed(srcMeta.getOffset()),
false);
srcDatums.put(srcProp, srcDatum);
}
if (dstDatum == null)
{
PropertyMeta dstMeta = dstProps.get(dstSetter);
// we are on a setter dst method, with a non-extent property which has an ASSIGN trigger
boolean dstAssignTrigger = dstSetter.getReturnType() != Void.class &&
dstMeta.getOriginalExtent() == 0 &&
dstBuf.getTriggerTracker() != null &&
dstBuf.getTriggerTracker().isTriggerEnabled(DatabaseEventType.ASSIGN,
dstProp,
dstBuf.logicalDatabase);
dstDatum = new DatumAccess(dstProp,
null,
dstSetter,
dstExtents.get(dstProp),
dstMeta,
dstBuf.isPropertyIndexed(dstMeta.getOffset()),
dstAssignTrigger);
dstDatums.put(dstProp, dstDatum);
}
copyMap.put(srcDatum, dstDatum);
}
}
return copyMap;
}
/**
* Helper method to report and possibly log a validation exception. Since validation exceptions generally
* are recoverable errors, they are logged only at debug level and finer.
*
* @param exc
* The validation exception.
* @param buffer
* The affected buffer.
* @param fieldUpdate
* if {@code true} the error message 142 is also displayed.
*/
protected static void reportValidationException(ValidationException exc,
RecordBuffer buffer,
boolean fieldUpdate)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, exc, "{%s} validation error", Utils.describeContext());
}
String bufName = buffer.getLegacyName();
int[] errCodes = new int[fieldUpdate ? 2 : 1];
String[] errMessages = new String[fieldUpdate ? 2 : 1];
if (fieldUpdate)
{
errCodes[1] = 142;
errMessages[1] = "Unable to update " + bufName + " Field";
}
// go up the exception chain and look for a SQL exception:
Throwable cause = exc.getCause();
while (cause != null &&
!(cause instanceof SQLException) &&
cause != cause.getCause())
{
cause = cause.getCause();
}
if (cause instanceof SQLException)
{
// 23001 - RESTRICT VIOLATION - restrict_violation
// 23503 - FOREIGN KEY VIOLATION - foreign_key_violation
// 23514 - CHECK VIOLATION - check_violation
SQLException sqle = (SQLException) cause;
if ("23502".equals(sqle.getSQLState()))
{
// 23502 - NOT NULL VIOLATION - not_null_violation
// heuristically detect the sql name of the column (seems to be the first word double quoted)
String msg = sqle.getMessage();
String fieldName = "?FIELD?";
int k1 = msg.indexOf('"') + 1;
if (k1 > 0)
{
int k2 = msg.indexOf('"', k1);
if (k2 > k1)
{
fieldName = msg.substring(k1, k2);
}
}
// heuristically convert from SQL name to legacy name
Iterator<Property> fields = buffer.getDmoInfo().getFields(false);
while (fields.hasNext())
{
Property prop = fields.next();
if (prop.column.equalsIgnoreCase(fieldName)) // ignore-case not necessary
{
fieldName = prop.legacy;
break;
}
}
// print the error message, defaulting to SQL column name if any heuristics failed
errCodes[0] = 275;
errMessages[0] = fieldName + " is mandatory, but has a value of ?";
}
else if (buffer.getDialect().isUniqueConstraintViolation(sqle))
{
// Depending on the dialect:
// 23000 - INTEGRITY CONSTRAINT VIOLATION - integrity_constraint_violation
// 23505 - UNIQUE VIOLATION - unique_violation
errCodes[0] = 132;
BitSet uIndex = null;
RecordMeta meta = buffer.getCurrentRecord()._getRecordMeta();
String[] uniqueIndexNames = meta.getUniqueIndexNames();
String message = cause.getMessage().toUpperCase();
for (int i = 0; i < uniqueIndexNames.length; i++)
{
String uniqueIndexName = uniqueIndexNames[i];
if (message.contains(uniqueIndexName.toUpperCase()))
{
uIndex = meta.getUniqueIndices()[i];
break;
}
}
errMessages[0] = (uIndex != null)
? Validation.getFailUniqueIndexMessage(uIndex, bufName, buffer.getCurrentRecord())
: bufName + " already exists with ? ?"; // failed to detect breached index
}
}
if (errMessages[0] == null)
{
errCodes[0] = exc.getNumber();
errMessages[0] = exc.getMessage();
}
ErrorManager.recordOrThrowError(errCodes, errMessages, true);
}
/**
* Get a local instance of the schema dictionary which is configured to resolve references
* related to the given list of buffers, when parsing a dynamic query or validation expression.
*
* @param buffers
* List of buffers referenced in the dynamic expression.
*
* @return Configured, context-local schema dictionary.
*/
static SchemaDictionary getSchemaDictionary(List<Buffer> buffers)
{
// the set of schema names referenced by the given list of buffers, alphabetically sorted
// to produce a consistent key to look up the cached SchemaDictionary
SortedSet<String> inUseSchemata = null;
// load program-specific databases (actually schema names); may be null if project is not
// configured to specify default databases by package
Set<String> defDbsByPkg = ProcedureManager.getDefaultDatabases();
// collect the subset of schema names needed by the schema dictionary to resolve database
// symbols for the tables in the given list of buffers
if (defDbsByPkg != null && !buffers.isEmpty())
{
inUseSchemata = new TreeSet<>();
// collect the names of schemas referenced by the buffers
for (Buffer buf : buffers)
{
RecordBuffer recBuf = ((BufferImpl) buf).buffer();
Database db = recBuf.getDatabase();
if (db.isMeta())
{
db = db.toType(Database.Type.PRIMARY);
}
String schema = DatabaseManager.getSchema(db);
// this is really just a sanity check, because if we hit a schema that wasn't in the
// default-databases configuration, there is something wrong with the caller's
// composition of the buffer list, or with the default-databases configuration
if (defDbsByPkg.contains(schema) || ConnectionManager.connected(db.getName()).booleanValue())
{
inUseSchemata.add(schema);
}
else if (!db.isTemporary() && LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, LOG_MSG_DB_UNEXP_REF, recBuf.getDMOAlias(), schema);
}
}
}
return SchemaDictionary.localInstance(inUseSchemata);
}
/**
* BUFFER-COPY statement runtime.
*
* Perform a bulk copy of data from the current source record (if any) to
* the current destination record. Data is copied between properties of
* like name. Other properties of the source and destination DMOs are
* ignored. If the destination buffer does not contain a current record,
* one is automatically created.
* <p>
* If a list of source properties is provided, the copy behaves
* differently, depending upon the value of <code>exclusive</code>:
* <ul>
* <li>if <code>true</code>, the listed properties are excluded from the copy;
* <li>if <code>false</code>, the listed properties are the only properties copied.
* </ul>.
* <p>
* The destination buffer is optionally validated after the copy.
*
* @param srcDmo
* Source DMO instance returned by a previous call to {@link #define}.
* @param dstDmo
* Destination DMO instance returned by a previous call to {@link #define}.
* @param srcFields
* Names of those source properties to be included or excluded from the copy,
* depending upon <code>exclusive</code>. May be <code>null</code>.
* @param exclusive
* <code>true</code> if <code>srcProps</code> represents an exclusive list;
* <code>false</code> if it represents an inclusive list.
* @param noLobs
* If {@code true}, exclude any BLOB/CLOB fields from the copy, else include these
* fields.
* @param validate
* <code>true</code> to validate the destination buffer after the copy.
*
* @throws ErrorConditionException
* if destination buffer is validated and fails; if there is no
* record in the source buffer.
*/
static void copy(DataModelObject srcDmo,
DataModelObject dstDmo,
String[] srcFields,
boolean exclusive,
boolean noLobs,
boolean validate)
{
/* TODO: update _UserTableStat counter(s) */
BufferReference dstProxy = (BufferReference) dstDmo;
BufferReference srcProxy = (BufferReference) srcDmo;
RecordBuffer srcBuf = srcProxy.buffer();
RecordBuffer dstBuf = dstProxy.buffer();
dstBuf.bufferManager.registerDirtyBuffer(dstBuf);
srcBuf.initialize();
dstBuf.initialize();
dstBuf.setBufferCopyDestination(true);
// Get source record. It is an error if there is none.
Record srcRec = srcBuf.getCurrentRecord();
if (srcRec == null)
{
String msg = "Source element of a BUFFER-COPY statement has no record";
ErrorManager.recordOrThrowError(5365, msg, false);
return;
}
// get destination record. Create one if necessary.
Record dstRec = dstBuf.getCurrentRecord();
if (dstRec == null)
{
dstBuf.create();
dstRec = dstBuf.getCurrentRecord();
}
if (srcRec == dstRec)
{
// already the same instance of the record
return;
}
boolean hasBefore = ((BufferImpl) dstProxy).isAfterBuffer() &&
dstBuf.getParentTable()._isTrackingChanges();
// make sure the destination buffer is aware of the fact that its fields were altered and prepare it for
// the write trigger
armWriteTrigger(dstBuf, true);
// try a fast copy first. If it worked, then we are done with the copy all together
if (fastCopy(srcBuf, dstBuf, srcFields != null && srcFields.length > 0, noLobs, validate, hasBefore))
{
Buffer bufferImpl = (Buffer) dstBuf.getDMOProxy();
if (validate)
{
if (bufferImpl.autoSynchronize().booleanValue())
{
bufferImpl.querySynchronize();
}
dstBuf.setBufferCopyDestination(false);
}
return;
}
Map<String, String> srcLegacyFields = srcBuf.getLegacyFieldNameMap().getLegacyField2Name();
Map<String, String> dstLegacyFields = dstBuf.getLegacyFieldNameMap().getLegacyField2Name();
// establish the set of fields to be copied
Set<String> lFields = getCommonFields(srcLegacyFields.keySet(),
dstLegacyFields.keySet(),
srcFields,
exclusive);
Map<DatumAccess, DatumAccess> copyMap = createSimplePropsMap(lFields,
srcBuf,
dstBuf,
srcBuf.getDmoInfo().getLegacyGetterMap(),
dstBuf.getDmoInfo().getLegacySetterMap(),
srcBuf.getDmoInfo().getExtentMap(),
dstBuf.getDmoInfo().getExtentMap(),
srcLegacyFields,
dstLegacyFields,
srcBuf.getDmoInfo().getPropsByGetterMap(),
dstBuf.getDmoInfo().getPropsBySetterMap(),
srcBuf.getGetterDatums(),
dstBuf.getSetterDatums(),
noLobs,
OperationType.COPY);
copy(srcBuf, dstBuf, copyMap, null, null, validate);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
*
* This method does a rough compare of any common fields, determined by name, data type, and
* extent-matching, between the source buffer and the target buffer. The resulting logical
* value is either TRUE or FALSE as a whole. A single field that does not compare causes the
* entire buffer to return FALSE. If there are fields in one buffer that do not exist in the
* other, they are ignored.
*
* @param srcBuf
* Source record buffer.
* @param dstBuf
* Destination record buffer.
* @param mode
* If mode-exp is given, it must evaluate to either "binary" or "case-sensitive"
* to provide that type of comparison. BUFFER-COMPARE( ) method supports binary
* and case-sensitive comparisons between CLOB as well as CHARACTER fields.
* @param except
* A character expression that evaluates to a comma-separated list of field names
* to be excluded from the compare.
* @param pairs
* A character expression that evaluates to a comma-delimited list of field-name
* pairs to be compared.
* @param noLobs
* If {@code true}, BLOB and CLOB fields are ignored during the comparison. May be
* {@code false} or {@code null} to include such fields.
*
* @return <code>true</code> if all fields values are equal, otherwise <code>false</code>.
*/
static logical compare(RecordBuffer srcBuf,
RecordBuffer dstBuf,
BufferCompare.Mode mode,
character except,
character pairs,
logical noLobs)
{
if (!doBuffersExist(srcBuf, dstBuf, MethodType.METHOD))
{
return new logical(false);
}
// Get maps of source getter methods and destination setter methods,
// each keyed by property name.
Map<String, Method> dstGetters = dstBuf.getDmoInfo().getLegacyGetterMap();
try
{
Map<DatumAccess, DatumAccess> propsMap = getPropsMap(srcBuf,
dstBuf,
dstGetters,
dstBuf.getDmoInfo().getPropsByGetterMap(),
dstBuf.getGetterDatums(),
except,
pairs,
noLobs,
OperationType.COMPARE,
null,
null);
if (propsMap == null)
{
return new logical(false);
}
else
{
List<String> diffs = compare(srcBuf, dstBuf, propsMap, mode, false);
return new logical(diffs != null && diffs.size() == 0);
}
}
catch (ErrorConditionException e)
{
return new logical(false);
}
}
/**
* Create a dynamic buffer for the permanent table.
*
* @param dmoIface
* DMO interface associated with the table.
* @param bufferName
* Target buffer name (4GL form).
* @param ldbName
* Logical name of permanent database associated with table.
*
* @return DMO proxy of the created buffer.
*/
static <T extends Buffer> DataModelObject createDynamicBufferForPermTable(
Class<?> dmoIface, String bufferName, String ldbName)
{
// TODO: not sure this is right...
Class<T> bufClass = (Class<T>) dmoIface.getDeclaredClasses()[0];
String uniqueBufferName = BufferManager.get().generateBufferName(bufferName);
DataModelObject buffer = (DataModelObject) RecordBuffer.define(
null, bufClass, ldbName, uniqueBufferName, bufferName, 0);
RecordBuffer recordBuffer = RecordBuffer.get(buffer);
recordBuffer.setDynamic();
((BufferImpl) buffer).interlink(); // HandleChain implementation
recordBuffer.openScopeAt(0);
return buffer;
}
/**
* Copy the content of a DMO to another. Only matching fields are processed. Only the fields
* are copied, the special and surrogate data is not copied. No errors are generated if fields
* are not in pairs and copy fails for them. The multiplexer, peer rowid and row state are
* not copied.
*
* @param target
* The target DMO.
* @param source
* The source DMO.
*/
static void copyCommonFields(Record target, Record source)
{
Method[] tMethods = target.getClass().getMethods();
for (Method tMethod : tMethods)
{
String tmName = tMethod.getName();
if (tmName.length() > 3 && !tmName.equals("setId") && tmName.startsWith("set"))
{
Method sMethod = null;
String smName = (tMethod.getReturnType() == logical.class ? "is" : "get") +
tmName.substring(3);
try
{
sMethod = source.getClass().getDeclaredMethod(smName);
}
catch (NoSuchMethodException e)
{
// silently skip this field
continue;
}
try
{
Utils.invoke(tMethod, target, Utils.invoke(sMethod, source));
}
catch (IllegalAccessException | InvocationTargetException e)
{
// silently skip this field
}
}
}
}
/**
* BUFFER-COPY() handle-based method runtime.
*
* This method copies any common fields, determined by name, data type, and extent-matching,
* from the source buffer to the receiving buffer. If there are fields in one buffer that do
* not exist in the other, they are ignored. This method is used to accommodate temp-tables
* of joins.
*
* @param dstBuf
* Destination record buffer.
* @param srcBuf
* Source record buffer.
* @param except
* A comma-separated list of fields that will be ignored in the copy process.
* @param pairs
* A character expression that evaluates to a comma-separated list of field-name
* pairs to be copied.
* @param noLobs
* If {@code true}, BLOB and CLOB fields are ignored during the comparison. May be
* {@code false} or {@code null} to include such fields.
*
* @return <code>true</code> if copy is successful, otherwise <code>false</code>.
*/
static logical copy(RecordBuffer dstBuf,
RecordBuffer srcBuf,
character except,
character pairs,
logical noLobs)
{
/* TODO: update _UserTableStat counter(s) */
Record srcRec = srcBuf.getCurrentRecord();
if (srcRec == null)
{
return new logical(false);
}
dstBuf.bufferManager.registerDirtyBuffer(dstBuf);
boolean created = false;
Record dstRec = dstBuf.getCurrentRecord();
if (dstRec == null)
{
// create destination record if destination buffer is empty
dstBuf.create();
created = true;
dstRec = dstBuf.getCurrentRecord();
}
BufferReference dstProxy = dstBuf.getDMOProxy();
Buffer srcProxy = (Buffer) srcBuf.getDMOProxy();
boolean hasBefore = ((BufferImpl) dstProxy).isAfterBuffer() &&
dstBuf.getParentTable()._isTrackingChanges();
boolean hasPairs = !(pairs == null || pairs.isUnknown() || pairs.getValue().length() == 0);
boolean hasExcept = !(except == null || except.isUnknown() || except.getValue().length() == 0);
List<DatumAccess> pkDstProps = new ArrayList<>();
List<DatumAccess> pkSrcProps = new ArrayList<>();
try
{
// make sure the destination buffer is aware of the fact that its fields were altered and prepare it
// for the write trigger
armWriteTrigger(dstBuf, false);
// try a fast copy first. If it worked, then we are done with the copy all together
if (fastCopy(srcBuf, dstBuf, hasPairs || hasExcept, noLobs.getValue(), true, hasBefore))
{
if (dstBuf.bufferCopyDestination)
{
if (srcProxy.autoSynchronize().booleanValue())
{
srcProxy.querySynchronize();
}
dstBuf.setBufferCopyDestination(false);
}
return new logical(true);
}
Map<String, Method> dstSetters = dstBuf.getDmoInfo().getLegacySetterMap();
Map<DatumAccess, DatumAccess> propsMap = getPropsMap(srcBuf,
dstBuf,
dstSetters,
dstBuf.getDmoInfo().getPropsBySetterMap(),
dstBuf.getSetterDatums(),
except,
pairs,
noLobs,
OperationType.COPY,
pkDstProps,
pkSrcProps);
if (propsMap != null)
{
return copy(srcBuf, dstBuf, propsMap, pkDstProps, pkSrcProps, true);
}
}
catch (ConditionException exc)
{
if (created && dstRec == dstBuf.getCurrentRecord())
{
try
{
dstBuf.delete();
}
catch (PersistenceException pe)
{
// already in exception handler
}
}
throw exc;
}
return new logical(false);
}
/**
* Get the buffer instance which corresponds with the given DMO proxy.
*
* @param dmo
* DMO instance returned by a previous call to {@link #define}.
*
* @return Buffer associated with <code>dmo</code>.
*/
static RecordBuffer get(DataModelObject dmo)
{
return ((BufferReference) dmo).buffer();
}
/**
* Reset to default values the state variables related to batch assign mode. If any dirty buffers have
* commits pending, these are rolled back now.
*
* @param bufferManager
* Context-local {@code BufferManager} instance.
*/
static void cleanupBatchMode(BufferManager bufferManager)
{
cleanupBatchMode(bufferManager.endBatchAssignMode());
}
/**
* Reset to default values the state variables related to batch assign mode. If any dirty buffers have
* commits pending, these are rolled back now.
*
* @param bufs
* Map of dirty batch assign mode buffers with
* corresponding code that restores their active state.
*/
static void cleanupBatchMode(Map<RecordBuffer, Runnable> bufs)
{
if (bufs == null)
{
return;
}
try
{
Iterator<RecordBuffer> iter = bufs.keySet().iterator();
while (iter.hasNext())
{
RecordBuffer buffer = iter.next();
if (buffer.isCommitPending())
{
buffer.setCommitPending(false);
try
{
buffer.getPersistence().rollback(!buffer.dmoInfo.multiTenant);
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
}
}
finally
{
// Reset the active state for each dirty buffer in the batch assign
Iterator<Runnable> iter = bufs.values().iterator();
Runnable resetActiveState;
while (iter.hasNext())
{
resetActiveState = iter.next();
if (resetActiveState != null)
{
resetActiveState.run();
}
}
bufs.clear();
}
}
/**
* Indicate whether two DMO instances represent the same record, as
* determined by a comparison of their primary key identifiers. To be
* considered the same, neither record may be <code>null</code>, nor have
* a <code>null</code> primary key identifier.
*
* @param dmo1
* First record to compare.
* @param dmo2
* Second record to compare.
*
* @return <code>true</code> if both records are non-<code>null</code>
* and have the same non-<code>null</code> primary keys.
*/
static boolean areDMOsSame(Record dmo1, Record dmo2)
{
if (dmo1 == null || dmo2 == null)
{
return false;
}
Long id1 = dmo1.primaryKey();
Long id2 = dmo2.primaryKey();
if (id1 == null || id2 == null)
{
return false;
}
return id1.equals(id2);
}
/**
* BUFFER-COMPARE statement runtime, returning the list of legacy fields that are different.
*
* Compare the contents of the properties which two DMO instances have in common. Common
* properties are defined as having the same legacy name and data type. Other properties are
* ignored for comparison purposes.
* <p>
* If a list of additional legacy field names is provided, the comparison behaves differently,
* depending upon the value of {@code exclusive}:
* <ul>
* <li>if {@code true}, the listed properties are excluded from the compare;</li>
* <li>if {@code false}, the listed properties are the only properties compared.</li>
* </ul>.
* <p>
* <b>Implementation Note:</b> this is not a full replacement for the Progress BUFFER-COMPARE
* statement. In particular, it does not handle the EXPLICIT COMPARES option.
*
* @param dmo1
* DMO instance returned by a previous call to {@link #define}.
* @param dmo2
* DMO instance returned by a previous call to {@link #define}.
* @param fieldList
* Legacy names of those fields to be included or excluded from the comparison,
* depending upon {@code exclusive}. May be {@code null}.
* @param noLobs
* If {@code true}, exclude any BLOB/CLOB fields from the comparison, else include
* these fields.
* @param mode
* Comparison mode. May be {@code null}.
* @param exclusive
* {@code true} if {@code fieldList} represents an exclusive list; {@code false} if
* it represents an inclusive list.
* @param exhaustive
* If {@code true}, all common properties are compared; if {@code false}, the
* comparison ends immediately after the first time a pair of properties fails to
* compare as equal.
*
* @return A list of names of those properties which failed to compare as equal. If empty, the
* two DMOs compared as equal overall; if not empty, the comparison determined one or
* more differences between them. Will return {@code null} only if one or both
* buffers are empty, indicating a comparison could not be performed.
*
* @throws ErrorConditionException
* if one or both buffers are empty, such that a comparison could not be performed;
* if there is a problem extracting data from a buffer.
*/
static List<String> compare(DataModelObject dmo1,
DataModelObject dmo2,
String[] fieldList,
boolean noLobs,
BufferCompare.Mode mode,
boolean exclusive,
boolean exhaustive)
{
RecordBuffer buf1 = ((BufferReference) dmo1).buffer();
RecordBuffer buf2 = ((BufferReference) dmo2).buffer();
if (!doBuffersExist(buf1, buf2, MethodType.STATEMENT))
{
return null;
}
Map<String, String> legacyFields1 = buf1.getLegacyFieldNameMap().getLegacyField2Name();
Map<String, String> legacyFields2 = buf2.getLegacyFieldNameMap().getLegacyField2Name();
// Establish the set of properties being compared.
Set<String> fields = getCommonFields(legacyFields1.keySet(),
legacyFields2.keySet(),
fieldList,
exclusive);
Map<DatumAccess, DatumAccess> propMap = createSimplePropsMap(fields,
buf1,
buf2,
buf1.getDmoInfo().getLegacyGetterMap(),
buf2.getDmoInfo().getLegacyGetterMap(),
buf1.getDmoInfo().getExtentMap(),
buf2.getDmoInfo().getExtentMap(),
legacyFields1,
legacyFields2,
buf1.getDmoInfo().getPropsByGetterMap(),
buf2.getDmoInfo().getPropsByGetterMap(),
buf1.getGetterDatums(),
buf2.getGetterDatums(),
noLobs,
OperationType.COMPARE);
return compare(buf1, buf2, propMap, mode, exhaustive);
}
/**
* Creates a proxy with an argument buffer as handler. This should be used when
* a buffer is referenced through an argument inside a procedure (by default buffers are
* input-output parameters). Although they are the same record buffer, the references
* should be distinct as they have different DMO aliases. This is why an argument buffer is
* actually a proxy to the original record buffer, not the buffer itself. Super class of the
* Proxy is the the class of the proxy from previous level. THe new proxy class generated will
* have the following format:
*
* class Proxy extends proxy.getClass()
* implements dmoBufIface, BufferReference, TempTableRecord?
* {
* ...
* }
*
* @param dmoBufIface
* The DMO buffer interface.
* @param proxy
* The buffer proxy.
* @param varName
* The new DMO alias which should be adopted by the record buffer when referenced
* through this argument buffer.
* @param legacyName
* The name which should be adopted by the buffer impl when referenced through this
* argument buffer.
* @return The argument buffer proxy.
*/
static <T> T makeArgumentBuffer(Class<T> dmoBufIface,
Buffer proxy,
String varName,
String legacyName)
{
BufferImpl buffer = (BufferImpl) proxy;
RecordBuffer rbuf = buffer.buffer();
BufferManager.registerPendingScopeable(rbuf.bufferManager);
Class<? extends DataModelObject> dmoIface = rbuf.getDMOInterface();
Class<?> dmoClass = buffer.buffer().getDMOImplementationClass();
Class<?>[] interfaces = TempRecord.class.isAssignableFrom(dmoClass)
? new Class[] { dmoBufIface, BufferReference.class, TempTableRecord.class }
: new Class[] { dmoBufIface, BufferReference.class };
try
{
InvocationHandler handler = new ArgumentBuffer(buffer, varName, legacyName);
@SuppressWarnings("unchecked")
T dmoProxy = ProxyFactory.getProxy(proxy.getClass(),
true,
interfaces,
false,
doNotProxyRes,
null,
handler);
return dmoProxy;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Define a new buffer which is based upon the specified interface. The buffer is created
* in read-only mode, temporarily for the scope of a trigger. It is in associated with the
* specified database.
*
* @param <T>
* Record type.
* @param def
* The currently executing OE class or external program.
* @param dmoBufIface
* Interface which defines DMO's public API plus all {@link Buffer} methods.
* @param database
* Name of the database which will be associated with the new record buffer.
* @param roValue
* The read-only value.
* @param fake
* {@code true} to mark this buffer as a "fake" buffer.
*
* @return An object which implements the specified interface, which has roValue as
* immutable data record.
*
* @see #isFake()
*/
private static <T extends Buffer> T defineReadOnlyImpl(Class<?> def,
Class<T> dmoBufIface,
String database,
Record roValue,
boolean fake)
{
RecordBuffer buffer = new RecordBuffer(def, dmoBufIface, database, null);
buffer.initialize(true);
buffer.setCurrentRecord(roValue, false, false, false, false);
buffer.setReadOnly(true);
buffer.setFake(fake);
return createProxy(dmoBufIface, buffer, null);
}
/**
* Makes sure the specified record buffer has the trigger tracked armed (ready to fire at next flush).
* If the buffer is null or does not have triggers, or it was already armed, nothing happens.
*
* @param rb
* The buffer to be armed.
* @param useOpenScope
* If {@code true} uses the number of open scopes to set DMO, else uses the
* scope depth at which transient record was created.
*/
private static void armWriteTrigger(RecordBuffer rb, boolean useOpenScope)
{
if (rb == null)
{
return;
}
TriggerTracker tt = rb.triggerTracker;
if (tt == null ||
tt.getOldDMO() != null ||
!tt.isTriggerEnabled(DatabaseEventType.WRITE, null, rb.logicalDatabase))
{
return; // no tracker, or already armed
}
if (rb.snapshot == null)
{
rb.snapshot = rb.currentRecord.snapshot();
}
if (useOpenScope)
{
tt.setOldDMO(rb.snapshot, rb.bufferManager.getOpenBufferScopes());
}
else
{
tt.setOldDMO(rb.snapshot, rb.createScope);
}
}
/**
* This method establishes the map of properties being compared or copied.
*
* @param srcBuf
* Source buffer for copy or compare operation.
* @param dstBuf
* Destination buffer for copy or compare operation.
* @param dstMethods
* Map of legacy field names to DMO getter or setter methods for the destination
* buffer.
* @param dstPropMeta
* Map of destination accessor methods to the property meta.
* @param dstDatums
* Map of destination properties to their {@link DatumAccess} instance.
* @param except
* A character expression that evaluates to a comma-separated list of field names
* to be excluded from the copy or compare operation.
* @param pairs
* A character expression that evaluates to a comma-delimited list of field-name
* pairs to be copied or compared.
* @param noLobs
* If {@code true}, exclude any BLOB/CLOB fields from the operation, else include
* these fields.
* @param opType
* Operation type (COMPARE/COPY).
* @param pkDstProps
* A list of <code>DatumAccess</code> objects where to copy the source PK (for pairs which are
* specified like <code>rowid(tt-src),tt-dst.f-rowid</code>). If <code>null</code>, these
* will not be reported back (BUFFER-COMPARE ignores these kind of mappings).
* @param pkSrcProps
* A list of <code>DatumAccess</code> objects where to copy the destination PK (for pairs which
* are specified like <code>rowid(tt-src),tt-dst.f-rowid</code>). If <code>null</code>, these
* will not be reported back (BUFFER-COMPARE ignores these kind of mappings).
*
* @return A map of <code>DatumAccess</code> objects for the source buffer to objects of the
* same type for the destination buffer, which is used to perform the copy or compare
* operation, or <code>null</code> if an error occurs during the mapping analysis.
*
* @throws ErrorConditionException
* if any Progress-defined error occurs during the mapping analysis and not in silent
* error mode.
*/
private static Map<DatumAccess, DatumAccess> getPropsMap(RecordBuffer srcBuf,
RecordBuffer dstBuf,
Map<String, Method> dstMethods,
Map<Method, PropertyMeta> dstPropMeta,
Map<String, DatumAccess> dstDatums,
character except,
character pairs,
logical noLobs,
OperationType opType,
List<DatumAccess> pkDstProps,
List<DatumAccess> pkSrcProps)
{
Map<String, String> srcFieldMap = srcBuf.getLegacyFieldNameMap().getLegacyField2Name();
Map<String, String> dstFieldMap = dstBuf.getLegacyFieldNameMap().getLegacyField2Name();
// collect the intersection of matching field names between the buffers; they will be
// checked for data type compatibility later
Map<String, String> sharedFieldMap = new HashMap<>(srcFieldMap);
sharedFieldMap.keySet().retainAll(dstFieldMap.keySet());
// add the PK, too
sharedFieldMap.put(Session.PK, Session.PK);
// process fields to be excluded, if any
if (except != null && !except.isUnknown() && except.getValue().length() > 0)
{
String excStr = except.toStringMessage();
List<String> exceptFields = StringHelper.parseTokens(excStr, ",", true, true, false);
for (String next : exceptFields)
{
FieldInfo info = null;
try
{
info = new FieldInfo(next);
}
catch (IllegalArgumentException e)
{
// P4GL silently discards bad field names
// spaces are NOT allowed within list
continue;
}
if (info.getExtentIndex() >= 0)
{
// this appears to be an error which reports a message and aborts the operation,
// but does not raise an error condition
String msg = "Exclude or Include field list may not have elements with subscripts";
ErrorManager.displayError(12414, msg, false);
return null;
}
sharedFieldMap.remove(info.getFieldName());
}
}
boolean skipLobs = noLobs != null && noLobs.booleanValue();
Map<String, Method> srcMethods = srcBuf.getDmoInfo().getLegacyGetterMap();
Map<String, Integer> srcExtents = srcBuf.getDmoInfo().getExtentMap();
Map<String, Integer> dstExtents = dstBuf.getDmoInfo().getExtentMap();
Map<Method, PropertyMeta> srcPropMeta = srcBuf.getDmoInfo().getPropsByGetterMap();
// create props map for shared fields, which must match in name, type, and extent, else they
// are discarded
Map<DatumAccess, DatumAccess> propsMap = new HashMap<>();
for (Map.Entry<String, String> sharedEntry : sharedFieldMap.entrySet())
{
String field = sharedEntry.getKey();
String srcProp = srcFieldMap.get(field);
String dstProp = dstFieldMap.get(field);
DatumAccess srcDatum = srcBuf.getGetterDatums().get(srcProp);
DatumAccess dstDatum = dstDatums.get(dstProp);
Method srcMeth = srcDatum == null ? srcMethods.get(srcProp) : srcDatum.getAccessor();
Method dstMeth = dstDatum == null ? dstMethods.get(dstProp) : dstDatum.getAccessor();
if (dstMeth == null)
{
continue;
}
if (isAssignableTypes(srcMeth, dstMeth, skipLobs, opType))
{
Integer srcExtent = srcDatum == null ? srcExtents.get(srcProp) : srcDatum.getExtent();
Integer dstExtent = dstDatum == null ? dstExtents.get(dstProp) : dstDatum.getExtent();
if ((srcExtent == null && dstExtent == null) ||
(srcExtent != null && dstExtent != null && srcExtent.equals(dstExtent)))
{
if (srcDatum == null)
{
PropertyMeta srcMeta = srcPropMeta.get(srcMeth);
srcDatum = new DatumAccess(srcProp,
null,
srcMeth,
srcExtent,
srcMeta,
srcBuf.isPropertyIndexed(srcMeta.getOffset()),
false);
srcBuf.getGetterDatums().put(srcProp, srcDatum);
}
if (dstDatum == null)
{
PropertyMeta dstMeta = dstPropMeta.get(dstMeth);
// we are on a setter dst method, with a non-extent property which has an ASSIGN trigger
boolean dstAssignTrigger =
dstMeth.getReturnType() != Void.class &&
dstMeta.getOriginalExtent() == 0 &&
dstBuf.getTriggerTracker() != null &&
dstBuf.getTriggerTracker().isTriggerEnabled(DatabaseEventType.ASSIGN,
dstProp,
dstBuf.logicalDatabase);
dstDatum = new DatumAccess(dstProp,
null,
dstMeth,
dstExtent,
dstMeta,
dstBuf.isPropertyIndexed(dstMeta.getOffset()),
dstAssignTrigger);
dstDatums.put(dstProp, dstDatum);
}
propsMap.put(srcDatum, dstDatum);
}
}
}
// if there is no explicit list of field pairs to be processed, we are done
if (pairs == null || pairs.isUnknown() || pairs.getValue().length() == 0)
{
return propsMap;
}
// process pair string, which can be in any order, can have buffer name qualifiers and
// extent index values
String pairsValue = pairs.getValue();
if (pairsValue != null && pairsValue.length() > 0)
{
List<String> pairsFields = StringHelper.parseTokens(pairsValue, ",", true, true, false);
int size = pairsFields.size();
// process each pair together
// note: the order of each pair (destination vs. source) does not matter, as long as the
// names are unambiguous, EXCEPT if a pair indicates 2 different elements of an extent
// field whose name is common to both buffers, the first of the pair is the destination
// and the second of the pair is the source; this works even if the fields are of
// different extents, as long as each index specified is legal for that buffer
for (int i = 0; i < size; i += 2)
{
// optimally would be to check the list size before processing it, but Progress
// processes it in groups of 2, only realizing very late that the list has an odd
// number of fields
if (i + 1 >= size)
{
String msg = "Pairs list had too many pairs or odd number of entries";
ErrorManager.recordOrThrowError(9035, msg, false);
return null;
}
String fieldSpec1 = pairsFields.get(i);
String fieldSpec2 = pairsFields.get(i + 1);
// another feature of Progress: if the first item in a pair starts with @ character,
// the entire pair is simply ignored.
// TODO: * are there other characters with this property ?
// * what is the reason for this 'feature'
if (fieldSpec1.startsWith("@"))
{
continue;
}
if (fieldSpec1.toLowerCase().startsWith("rowid("))
{
fieldSpec1 = fieldSpec1.substring(fieldSpec1.indexOf('(') + 1, fieldSpec1.indexOf(')'));
fieldSpec1 = fieldSpec1 + "." + Session.PK;
}
if (fieldSpec2.toLowerCase().startsWith("rowid("))
{
fieldSpec2 = fieldSpec2.substring(fieldSpec2.indexOf('(') + 1, fieldSpec2.indexOf(')'));
fieldSpec2 = fieldSpec2 + "." + Session.PK;
}
String srcPropF1 = null;
String srcPropF2 = null;
String dstPropF1 = null;
String dstPropF2 = null;
String srcProp = null;
String dstProp = null;
String srcKey = null;
String dstKey = null;
boolean ext1Valid = false;
boolean ext2Valid = false;
Integer srcExtent = null;
Integer dstExtent = null;
try
{
FieldInfo info1 = null;
FieldInfo info2 = null;
try
{
// if the second field is bad, message is still about fieldSpec1
info1 = new FieldInfo(fieldSpec1);
info2 = new FieldInfo(fieldSpec2);
}
catch (IllegalArgumentException e)
{
String msg = String.format("Pair with %s failed to have a member in each table",
fieldSpec1);
if (opType == OperationType.COPY)
{
ErrorManager.recordOrThrowError(9034, msg, false);
}
else
{
ErrorManager.recordOrShowError(9034, msg, false, false);
}
return null;
}
String field1 = info1.getFieldName();
String field2 = info2.getFieldName();
srcPropF1 = (srcBuf.checkBufferName(info1) ? srcFieldMap.get(field1) : null);
srcPropF2 = (srcBuf.checkBufferName(info2) ? srcFieldMap.get(field2) : null);
dstPropF1 = (dstBuf.checkBufferName(info1) ? dstFieldMap.get(field1) : null);
dstPropF2 = (dstBuf.checkBufferName(info2) ? dstFieldMap.get(field2) : null);
if (Session.PK.equals(field1))
{
if (srcBuf.getLegacyName().equalsIgnoreCase(info1.getBufferName()))
{
srcPropF1 = Session.PK;
}
else if (dstBuf.getLegacyName().equalsIgnoreCase(info1.getBufferName()))
{
dstPropF1 = Session.PK;
}
else if (dstPropF2 != null)
{
// dst was alredy resolved, we can assume PK is for src
srcPropF1 = Session.PK;
}
}
if (Session.PK.equals(field2))
{
if (srcBuf.getLegacyName().equalsIgnoreCase(info2.getBufferName()))
{
srcPropF2 = Session.PK;
}
else if (dstBuf.getLegacyName().equalsIgnoreCase(info2.getBufferName()))
{
dstPropF2 = Session.PK;
}
else if (srcPropF1 != null)
{
// src was alredy resolved, we can assume PK is for dst
dstPropF2 = Session.PK;
}
}
// last ditch effort: for any null results, try using the entire field spec as a
// key, because even though the dot is used to delimit a buffer name from a field
// name, it also is a legal character within a field name
String full1 = info1.getFullSpec();
String full2 = info2.getFullSpec();
if (dstPropF1 == null)
{
dstPropF1 = dstFieldMap.get(full1);
}
if (dstPropF2 == null)
{
dstPropF2 = dstFieldMap.get(full2);
}
if (srcPropF1 == null)
{
srcPropF1 = srcFieldMap.get(full1);
}
if (srcPropF2 == null)
{
srcPropF2 = srcFieldMap.get(full2);
}
int e1 = info1.getExtentIndex();
int e2 = info2.getExtentIndex();
boolean isExtent = (e1 >= 0 || e2 >= 0);
Integer extent1 = (e1 < 0 ? null : e1);
Integer extent2 = (e2 < 0 ? null : e2);
// try to match first of pair with destination and second of pair with end and if
// that is not possible, try it the other way around; note that in the event the
// field name exists in both the source and destination buffer, Progress always
// seems to treat the first item as the destination and the second as the source
if (srcPropF2 != null && dstPropF1 != null)
{
ext2Valid = (e2 < 0 || srcBuf.isValidExtent(e2, srcPropF2));
ext1Valid = (e1 < 0 || dstBuf.isValidExtent(e1, dstPropF1));
if (ext2Valid && ext1Valid)
{
srcProp = srcPropF2;
srcExtent = extent2;
dstProp = dstPropF1;
dstExtent = extent1;
// lookup keys for DatumAccess objects must include the specific extent field element
srcKey = extent2 == null ? srcProp : srcProp + '[' + extent2 + ']';
dstKey = extent1 == null ? dstProp : dstProp + '[' + extent1 + ']';
}
}
else if (srcPropF1 != null && dstPropF2 != null)
{
ext1Valid = (e1 < 0 || srcBuf.isValidExtent(e1, srcPropF1));
ext2Valid = (e2 < 0 || dstBuf.isValidExtent(e2, dstPropF2));
if (ext1Valid && ext2Valid)
{
srcProp = srcPropF1;
srcExtent = extent1;
dstProp = dstPropF2;
dstExtent = extent2;
// lookup keys for DatumAccess objects must include the specific extent field element
srcKey = extent1 == null ? srcProp : srcProp + '[' + extent1 + ']';
dstKey = extent2 == null ? dstProp : dstProp + '[' + extent2 + ']';
}
}
else
{
// if we matched a destination buffer, but not a source buffer, check whether
// the error was an improper buffer qualifier on the source and report the
// error accordingly
if (dstPropF1 != null && !srcBuf.reportBadBufferName(info2, fieldSpec2))
{
return null;
}
if (dstPropF2 != null && !srcBuf.reportBadBufferName(info1, fieldSpec1))
{
return null;
}
// report error if we didn't get hits in both tables
String msg = String.format("Pair with %s failed to have a member in each table",
trimFieldSpec(fieldSpec1, info1));
if (opType == OperationType.COPY)
{
ErrorManager.recordOrThrowError(9034, msg, false);
}
else
{
ErrorManager.recordOrShowError(9034, msg, false, false);
}
return null;
}
// report error if either specified extent is out of bounds
if (isExtent && (!ext1Valid || !ext2Valid))
{
String failedSpec;
FieldInfo failedInfo;
if (ext1Valid)
{
failedSpec = fieldSpec2;
failedInfo = info2;
}
else
{
failedSpec = fieldSpec1;
failedInfo = info1;
}
String msg = String.format("Array index %d for %s is out of range",
failedInfo.getExtentIndex() + 1,
trimFieldSpec(failedSpec, failedInfo));
ErrorManager.recordOrThrowError(11856, msg, false);
return null;
}
// report error if extent types are mismatched in pair (i.e., one spec refers to a
// scalar field (including a single element of an extent field, the other refers to
// an extent field)
boolean srcScalar = !srcExtents.containsKey(srcProp) || srcExtent != null;
boolean dstScalar = !dstExtents.containsKey(dstProp) || dstExtent != null;
if (srcScalar != dstScalar)
{
String msg = String.format("Pair extents do not match for %s and %s",
trimFieldSpec(fieldSpec1, info1),
trimFieldSpec(fieldSpec2, info2));
ErrorManager.recordOrThrowError(11857, msg, false);
return null;
}
// Progress also reports 11857 error both for BUFFER-COPY and BUFFER-COMPARE if
// mapped fields have different extent values
if (srcExtent == null &&
dstExtent == null &&
srcExtents.containsKey(srcProp) &&
dstExtents.containsKey(dstProp))
{
// compare extent sizes
Integer srcExtentSize = srcExtents.get(srcProp);
if (srcExtentSize != null)
{
Integer dstExtentSize = dstExtents.get(dstProp);
if (!srcExtentSize.equals(dstExtentSize))
{
String msg = String.format("Pair extents do not match for %s and %s",
trimFieldSpec(fieldSpec1, info1),
trimFieldSpec(fieldSpec2, info2));
ErrorManager.recordOrThrowError(11857, msg, false);
return null;
}
}
}
}
catch (IllegalArgumentException exc)
{
// an invalid field spec does not cause an error, but the pair containing the
// invalid entry is ignored
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Invalid field specifier: ", exc);
}
continue;
}
// if we made it through all of the above checks, do one final test to determine that
// the paired fields are data-type-compatible, then add them to the props map
DatumAccess srcDatum = srcBuf.getGetterDatums().get(srcKey);
DatumAccess dstDatum = dstDatums.get(dstKey);
Method srcMeth = srcDatum == null ? srcMethods.get(srcProp) : srcDatum.getAccessor();
Method dstMeth = dstDatum == null ? dstMethods.get(dstProp) : dstDatum.getAccessor();
if (srcMeth == null && Session.PK.equals(srcProp))
{
srcMeth = BaseRecord.PK_GETTER;
}
if (dstMeth == null && Session.PK.equals(dstProp))
{
dstMeth = BaseRecord.PK_GETTER;
}
if (isAssignableTypes(srcMeth, dstMeth, skipLobs, opType))
{
if (srcDatum == null && srcMeth != BaseRecord.PK_GETTER)
{
PropertyMeta srcMeta = srcPropMeta.get(srcMeth);
srcDatum = new DatumAccess(srcProp,
srcExtent,
srcMeth,
srcExtents.get(srcProp),
srcMeta,
srcBuf.isPropertyIndexed(srcMeta.getOffset()),
false);
srcBuf.getGetterDatums().put(srcKey, srcDatum);
}
if (dstDatum == null && dstMeth != BaseRecord.PK_GETTER)
{
PropertyMeta dstMeta = dstPropMeta.get(dstMeth);
// we are on a setter dst method, with a non-extent property which has an ASSIGN trigger
boolean dstAssignTrigger =
dstMeth.getReturnType() != Void.class &&
dstMeta.getOriginalExtent() == 0 &&
dstBuf.getTriggerTracker() != null &&
dstBuf.getTriggerTracker().isTriggerEnabled(DatabaseEventType.ASSIGN,
dstProp,
dstBuf.logicalDatabase);
dstDatum = new DatumAccess(dstProp,
dstExtent,
dstMeth,
dstExtents.get(dstProp),
dstMeta,
dstBuf.isPropertyIndexed(dstMeta.getOffset()),
dstAssignTrigger);
dstDatums.put(dstKey, dstDatum);
}
if (srcMeth == BaseRecord.PK_GETTER)
{
if (pkDstProps != null)
{
pkDstProps.add(dstDatum);
}
}
else if(dstMeth == BaseRecord.PK_GETTER)
{
if (pkSrcProps != null)
{
pkSrcProps.add(srcDatum);
}
}
else
{
propsMap.put(srcDatum, dstDatum);
}
}
else
{
String msg = "Datatypes in pairlist argument for ATTACH-DATA-SOURCE, BUFFER-COPY" +
" or BUFFER-COMPARE methods must match for fields " + fieldSpec1 +
" and " + fieldSpec2;
ErrorManager.recordOrThrowError(12860, msg, false);
return null;
}
}
}
return propsMap;
}
/**
* Trim the extent dereference portion (if any) from a field specifier and return the root
* specifier, including buffer name qualifier (if any).
*
* @param fieldSpec
* Full field specifier.
* @param fieldInfo
* <code>FieldInfo</code> object which represents the given specifier.
*
* @return The trimmed field specifier.
*/
private static String trimFieldSpec(String fieldSpec, FieldInfo fieldInfo)
{
int index = fieldInfo.getExtentIndex();
if (index < 0)
{
return fieldSpec;
}
int trim = String.valueOf(index).length() + 2;
return (fieldSpec.substring(0, fieldSpec.length() - trim));
}
/**
* Helper method to format a property value. To be used for debug only.
*
* @param value
* {@code BaseDataType} or {@code Object} property value.
*
* @return Formatted string representation of the value.
*/
private static String formatProperty(Object value)
{
if (value == null)
{
return "?";
}
else if (value instanceof BaseDataType)
{
return ((BaseDataType) value).toStringMessage();
}
else if (value.getClass().isArray())
{
if (value.getClass().getComponentType().isPrimitive())
{
int arrlength = Array.getLength(value);
if (arrlength == 0)
{
return "[]";
}
StringBuilder b = new StringBuilder();
b.append('[');
for (int i = 0; ; i++)
{
if (i == arrlength)
{
return b.append(']').toString();
}
b.append(Array.get(value, i));
b.append(", ");
}
}
return Arrays.toString((Object[]) value);
}
return String.valueOf(value);
}
/**
* This method checks that two getter or setter methods have compatible corresponding return
* or parameter types for given operation type.
*
* @param srcMethod
* Source method for given operation.
* @param dstMethod
* Destination method for given operation.
* @param noLobs
* If {@code true}, return {@code false} if the source method has a BLOB/CLOB return
* type.
* @param operationType
* Operation type (COMPARE/COPY).
*
* @return <code>true</code> if methods are compatible, otherwise <code>false</code>.
*/
private static boolean isAssignableTypes(Method srcMethod,
Method dstMethod,
boolean noLobs,
OperationType operationType)
{
if (operationType == OperationType.COMPARE)
{
// values for srcParameterTypes.length and dstParameterTypes.length (both getters)
// are 0, 1:
// getF1() and getF2() or
// getF1(int index) and getF2(int index)
Class<?>[] srcParameterTypes = srcMethod.getParameterTypes();
Class<?>[] dstParameterTypes = dstMethod.getParameterTypes();
if (srcParameterTypes.length != dstParameterTypes.length)
{
return false;
}
}
Class<?> srcMethodType = DBUtils.getGetterSetterType(srcMethod);
// if noLobs == true, return false for BLOB/CLOB field
if (noLobs && (srcMethodType.equals(blob.class) || srcMethodType.equals(clob.class)))
{
return false;
}
Class<?> dstMethodType = DBUtils.getGetterSetterType(dstMethod);
if (operationType == OperationType.COMPARE)
{
return dstMethodType.equals(srcMethodType);
}
else
{
return dstMethodType.isAssignableFrom(srcMethodType);
}
}
/**
* This method checks that two buffer correspond not null records and displayOrRecordError
* otherwise.
*
* @param srcBuf
* Source record buffer.
* @param dstBuf
* Destination record buffer.
*
* @return <code>true</code> if both records are not nulls, otherwise <code>false</code>.
*/
private static boolean doBuffersExist(RecordBuffer srcBuf, RecordBuffer dstBuf, MethodType methodType)
{
srcBuf.initialize();
dstBuf.initialize();
if (srcBuf.getCurrentRecord() != null && dstBuf.getCurrentRecord() != null)
{
return true;
}
if (methodType == MethodType.METHOD)
{
String msg = "BUFFER-COMPARE must have records in both target and source buffers";
ErrorManager.recordOrShowError(9039, msg, false);
}
else
{
if (srcBuf.getCurrentRecord() == null)
{
String msg = "Source element of a BUFFER-COMPARE statement has no record";
ErrorManager.recordOrThrowError(5369, msg, false);
}
else
{
String msg = "Target element of a BUFFER-COMPARE statement has no record";
ErrorManager.recordOrThrowError(5370, msg, false);
}
}
return false;
}
/**
* Sets compare mode to data type instance for BUFFER-COPY and BUFFER-COMPARE methods.
* <p>
* #2080, note 93: ... when you get the value for a DMO property (via getter.invoke), you
* get a copy, not the actual reference which is saved at the DMO. Thus, is enough to use
* setCaseSensitive() for that Text instance, without having to worry to restore the
* previous state (as we work with a copy which will get discarded once its job is done).
* About the threading issue: Text.caseSens is a field which is set per each
* character/longchar var instance. Currently, all communication is done sync (with some few
* CTRL-C exceptions), so no two threads should be able to change the Text.caseSens field
* for the same instance.
*
* @param datum
* Data type instance.
* @param mode
* A buffer compare mode constant.
*
* @return The buffer compare mode needed for the datum's type; if no special mode is
* required based on the datum's type, return DEFAULT, even if a different mode was
* passed in.
*/
private static BufferCompare.Mode setCompareMode(BaseDataType datum, BufferCompare.Mode mode)
{
switch (mode)
{
case CASE_SENSITIVE:
if (datum instanceof Text)
{
((Text) datum).setCaseSensitive(true);
return mode;
}
break;
case BINARY:
if (datum instanceof clob || datum instanceof blob)
{
return mode;
}
break;
case DEFAULT:
break;
}
return BufferCompare.Mode.DEFAULT;
}
/**
* This method compares given fields between the source buffer and the target buffer.
*
* @param srcBuf
* Source record buffer.
* @param dstBuf
* Destination record buffer.
* @param propsMap
* Map of properties in source record buffer to properties in destination
* record buffer
* @param mode
* If mode-exp is given, it must evaluate to either "binary" or "case-sensitive"
* to provide that type of comparison.BUFFER-COMPARE( ) method supports binary
* and case-sensitive comparisons between CLOB as well as CHARACTER fields.
* @param exhaustive
* if <code>true</code>, all common properties are compared; if <code>false</code>,
* the comparison ends immediately after the first time a pair of properties fails
* to compare as equal.
*
* @return A list of fields which failed to compare as equal. If empty, the two buffers
* compared as equal overall; if not empty, the comparison determined one or more
* differences between them. Will return <code>null</code> to indicate a comparison
* could not be performed.
*/
private static List<String> compare(RecordBuffer srcBuf,
RecordBuffer dstBuf,
Map<DatumAccess, DatumAccess> propsMap,
BufferCompare.Mode mode,
boolean exhaustive)
{
if (propsMap == null)
{
return null;
}
Record srcRec = srcBuf.getCurrentRecord();
Record dstRec = dstBuf.getCurrentRecord();
try
{
Map<String, String> srcName2LegacyField = srcBuf.getLegacyFieldNameMap().getName2LegacyField();
List<String> diffs = null;
for (Map.Entry<DatumAccess, DatumAccess> entry : propsMap.entrySet())
{
DatumAccess daSrc = entry.getKey();
DatumAccess daDst = entry.getValue();
String srcProp = daSrc.getPropertyName();
String dstProp = daDst.getPropertyName();
PropertyMeta srcPropMeta = daSrc.getPropertyMeta();
PropertyMeta dstPropMeta = daDst.getPropertyMeta();
DataHandler srcHandler = srcPropMeta.getDataHandler();
DataHandler dstHandler = dstPropMeta.getDataHandler();
Integer srcExtent = daSrc.getExtent();
Integer dstExtent = daDst.getExtent();
if (srcExtent == null)
{
srcExtent = 0;
}
if (dstExtent == null)
{
dstExtent = 0;
}
if (srcExtent.intValue() != dstExtent.intValue())
{
// TODO: is this an error?
continue;
}
boolean test;
if (srcExtent == 0)
{
// Simple getter invocations.
BaseDataType d1 = srcHandler.getField(srcRec, srcPropMeta.getOffset());
BaseDataType d2 = dstHandler.getField(dstRec, dstPropMeta.getOffset());
if (setCompareMode(d1, mode) == BufferCompare.Mode.BINARY)
{
test = LargeObject.binaryEquals((LargeObject) d1, (LargeObject) d2);
}
else
{
setCompareMode(d2, mode);
test = d1.equals(d2);
}
if (!test)
{
// Populate the list with legacy field names instead of post-conversion DMO
// property names.
if (diffs == null)
{
diffs = new ArrayList<>();
}
diffs.add(srcName2LegacyField.get(srcProp));
if (!exhaustive)
{
return diffs;
}
}
}
else
{
// indexed getter and setter invocations.
for (int i = 0; i < srcExtent; i++)
{
BaseDataType d1 = srcHandler.getField(srcRec, srcPropMeta.getOffset() + i);
BaseDataType d2 = dstHandler.getField(dstRec, dstPropMeta.getOffset() + i);
if (setCompareMode(d1, mode) == BufferCompare.Mode.BINARY)
{
test = LargeObject.binaryEquals((LargeObject) d1, (LargeObject) d2);
}
else
{
setCompareMode(d2, mode);
test = d1.equals(d2);
}
if (!test)
{
// populate the list with legacy field names instead of post-conversion
// DMO property names.
if (diffs == null)
{
diffs = new ArrayList<>();
}
diffs.add(srcName2LegacyField.get(srcProp));
if (!exhaustive)
{
return diffs;
}
// if at least one element from the extent is different, the whole field
// is marked as different, we don't need to check the others
break;
}
}
}
}
if (diffs == null)
{
diffs = Collections.emptyList();
}
return diffs;
}
catch (ConditionException exc)
{
DBUtils.handleException(srcBuf.getDatabase(), exc);
DBUtils.handleException(dstBuf.getDatabase(), exc);
if (!srcBuf.txHelper.errHlp.isSilent())
{
throw exc;
}
}
catch (Exception exc)
{
DBUtils.handleException(srcBuf.getDatabase(), exc);
DBUtils.handleException(dstBuf.getDatabase(), exc);
String msg = "Error performing buffer compare";
LOG.log(Level.SEVERE, msg, exc);
// TODO: use proper error.
ErrorManager.recordOrThrowError(-1, msg);
}
return null;
}
/**
* This method copies given fields from the source buffer to the receiving buffer.
*
* @param srcBuf
* Source record buffer.
* @param dstBuf
* Destination record buffer.
* @param propsMap
* Map of properties in source record buffer to properties in destination record buffer
* @param validate
* {@code true} to validate the destination buffer after the copy.
* @param pkDstProps
* List of destination table <code>DatumAccess</code> instances where to copy the source PK.
* @param pkSrcProps
* List of source table <code>DatumAccess</code> instances where to copy the destination PK.
* @return {@code true} if copy is successful, otherwise {@code false}.
*/
private static logical copy(RecordBuffer srcBuf,
RecordBuffer dstBuf,
Map<DatumAccess, DatumAccess> propsMap,
List<DatumAccess> pkDstProps,
List<DatumAccess> pkSrcProps,
boolean validate)
{
/* TODO: update _UserTableStat counter(s) */
BufferReference srcProxy = srcBuf.getDMOProxy();
BufferReference dstProxy = dstBuf.getDMOProxy();
// Perform the copy. The record, if newly created, will automatically be flushed to the
// database once all properties in any index have been updated, or once the copy is complete
// if validation is requested.
boolean inBatch = dstBuf.getBufferManager().isBatchAssignMode();
boolean error = false;
boolean batchError = true;
logical result;
try
{
if (validate)
{
startBatch(srcBuf.bufferManager, true);
}
Record dstRecord = dstProxy.buffer().getCurrentRecord();
Runnable restoreActiveStateDst = null;
Runnable restoreActiveStateSrc = null;
try
{
// perform the copy
dstBuf.setBulkCopy(true);
restoreActiveStateDst = dstRecord.setActiveBuffer(dstBuf);
result = copy(srcProxy, dstProxy, propsMap, pkDstProps);
if (result.getValue() && pkSrcProps != null && !pkSrcProps.isEmpty())
{
restoreActiveStateSrc = srcBuf.getCurrentRecord().setActiveBuffer(srcBuf);
// copy the dst PK to pkSrcProps in srcBuf
rowid dstPk = new rowid(dstRecord.primaryKey());
for (DatumAccess da : pkSrcProps)
{
BufferImpl deferBuff = (BufferImpl) srcBuf.buffer().getDMOProxy();
deferBuff.dereference(da.propertyMeta.getLegacyName(), dstPk);
}
if (srcBuf.getCurrentRecord().checkState(DmoState.CHANGED))
{
srcBuf.getBufferManager().addDirtyBatchBuffer(srcBuf, restoreActiveStateSrc);
}
}
if (dstRecord.checkState(DmoState.CHANGED))
{
dstBuf.getBufferManager().addDirtyBatchBuffer(dstBuf, restoreActiveStateDst);
}
batchError = false;
}
finally
{
if (validate)
{
endBatch(srcBuf.bufferManager, batchError);
}
}
if (result.getValue())
{
dstBuf.markChangeScope();
}
return result;
}
catch (ConditionException exc)
{
error = true;
throw exc;
}
catch (Exception exc)
{
error = true;
DBUtils.handleException(srcBuf.getDatabase(), exc);
DBUtils.handleException(dstBuf.getDatabase(), exc);
String msg = "Error performing buffer copy";
LOG.log(Level.SEVERE, msg, exc);
// TODO: use proper error.
ErrorManager.recordOrThrowError(-1, msg);
}
finally
{
dstBuf.setBulkCopy(false);
if (dstBuf.isCommitPending() && !inBatch)
{
try
{
if (!error)
{
dstBuf.getPersistence().commit(dstBuf.getPersistenceContext());
}
else
{
dstBuf.getPersistence().rollback(dstBuf.getPersistenceContext());
}
}
catch (Exception exc)
{
DBUtils.handleException(srcBuf.getDatabase(), exc);
DBUtils.handleException(dstBuf.getDatabase(), exc);
String msg = "Error " + (error ? "rolling back" : "committing") + " during buffer copy";
LOG.log(Level.SEVERE, msg, exc);
}
finally
{
dstBuf.setCommitPending(false);
}
}
}
return new logical(false);
}
/**
* Processes validation rules, error checking and any other user-defined constraint processing
* that needs to be applied. This is used to check if a proposed update to an object will
* pass validation. If the method silently returns then all tests have passed, otherwise
* an exception will be thrown on any validation failure.
*
* @param valexp
* The validation expression to evaluate.
* @param valmsg
* The error message to display when validation fails.
*
* @return <code>true</code> if the validation succeeded and the record can be deleted.
*
* @throws ErrorConditionException
* if the database trigger or the validation will fail
*/
private static boolean validate(Supplier<logical> valexp, Supplier<character> valmsg)
{
boolean ok = false;
try
{
ok = valexp.get().booleanValue();
}
catch (ErrorConditionException e)
{
// TODO: if we don't have to raise an END condition, this can be removed and we can
// use the common code below
ErrorManager.recordOrThrowError(0, valmsg.get().toStringMessage(), false, true);
// TODO: why are we raising the END condition here? It seems wrong.
throw new EndConditionException(e);
}
if (!ok)
{
// check IF this even generates an error condition AND what the err num should
// be if it does AND IF it honors NO-ERROR (which definitely can be applied to
// the delete statement)
ErrorManager.recordOrThrowError(0, valmsg.get().toStringMessage(), false, true);
}
return ok;
}
/**
* Fast way to do the buffer-copy. This doesn't work in all scenarios, but can greatly fasten
* some cases (which are often in practice). This will check if the current scenario is trivial:
* no assign triggers, no before buffers, no explicit pair copy and explicit dmo signature match.
* For this case, the fields are set in bulk through direct access.
*
* @param srcBuf
* The source buffer from which the copy is done.
* @param dstBuf
* The destination buffer to which the copy is done.
* @param forcePair
* Flag to indicate if an explicit pair of fields should be copied / not copied.
* @param noLobs
* Flag to indicate if lobs should be omitted from the copy.
* @param validate
* {@code true} to validate the destination buffer after the copy.
* @param hasBefore
* Flag to indicate if the buffer have before buffer which should be also copied.
*
* @return {@code true} if the fast copy is eligible and could be done.
* {@code false} if the fast copy couldn't be applied for these buffers.
*/
private static boolean fastCopy(RecordBuffer srcBuf,
RecordBuffer dstBuf,
boolean forcePair,
boolean noLobs,
boolean validate,
boolean hasBefore)
{
if (noLobs || forcePair || hasBefore)
{
return false;
}
DmoMeta srcMeta = srcBuf.getDmoInfo();
DmoMeta dstMeta = dstBuf.getDmoInfo();
DmoSignature srcSignature = srcMeta.getSignature();
DmoSignature dstSignature = dstMeta.getSignature();
if (!DMOSignatureHelper.validBufferCopy(srcSignature, dstSignature) ||
dstBuf.triggerTracker != null &&
dstBuf.triggerTracker.hasAnyAssignTrigger(dstMeta, dstBuf.logicalDatabase))
{
return false;
}
// all the field names are both in the source and the destination
// at this point the DMOs are similar enough to fasten the copy
boolean success = true;
Record srcRec = srcBuf.getCurrentRecord();
Record dstRec = dstBuf.getCurrentRecord();
if (DMOSignatureHelper.exactPropertyOrder(srcSignature, dstSignature))
{
// the fields are also in the same order, which means we can assign directly the values
// from the source to the destination
boolean batchError = true;
boolean error = false;
boolean inBatch = dstBuf.getBufferManager().isBatchAssignMode();
try
{
if (validate)
{
startBatch(srcBuf.bufferManager, true);
}
Runnable restoreActiveState = dstRec.setActiveBuffer(dstBuf);
try
{
dstBuf.setBulkCopy(true);
success &= OrmUtils.setAllFields(srcRec, dstRec);
if (dstRec.checkState(DmoState.CHANGED))
{
dstBuf.getBufferManager().addDirtyBatchBuffer(dstBuf, restoreActiveState);
}
batchError = false;
}
finally
{
if (validate)
{
endBatch(srcBuf.bufferManager, batchError);
}
}
if (success)
{
dstBuf.markChangeScope();
return true;
}
else
{
// otherwise, fallback to the classic implementation
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, LOG_MSG_SIGN_BROKEN);
}
return false;
}
}
catch (ConditionException exc)
{
error = true;
throw exc;
}
catch (Exception exc)
{
error = true;
DBUtils.handleException(srcBuf.getDatabase(), exc);
DBUtils.handleException(dstBuf.getDatabase(), exc);
String msg = "Error performing buffer copy";
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE, msg, exc);
}
// TODO: use proper error.
ErrorManager.recordOrThrowError(-1, msg);
return false;
}
finally
{
dstBuf.setBulkCopy(false);
if (dstBuf.isCommitPending() && !inBatch)
{
try
{
if (!error)
{
dstBuf.getPersistence().commit(dstBuf.getPersistenceContext());
}
else
{
dstBuf.getPersistence().rollback(dstBuf.getPersistenceContext());
}
}
catch (Exception exc)
{
DBUtils.handleException(srcBuf.getDatabase(), exc);
DBUtils.handleException(dstBuf.getDatabase(), exc);
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE,
exc,
"Error %s during buffer copy",
error ? "rolling back" : "committing");
}
}
finally
{
dstBuf.setCommitPending(false);
}
}
}
}
else
{
// TODO: implement a faster copy in case the signatures match but the field order doesn't match
if (unimplementedFastCopyWarningCounter < 10 && LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, LOG_MSG_SIGN_UNIMPL);
if (++unimplementedFastCopyWarningCounter == 10)
{
LOG.log(Level.WARNING,
"10 warnings were printed. No further similar messages will be printed.");
}
}
return false;
}
}
/**
* Get the {@link FastFindCache} instance for the database associated with this buffer.
*
* @return See above.
*/
public FastFindCache getFastFindCache()
{
return persistenceContext.getFastFindCache();
}
/**
* Get the {@link #undoable} instance.
*
* @return See above.
*/
public LazyUndoable getUndoable()
{
return undoable;
}
/**
* Report the record buffers for whose changes this object is interested
* in listening. This listener will be registered with the {@link
* ChangeBroker} to receive notifications of any change to DMOs whose
* interface types match those returned by the {@link
* RecordBuffer#getDMOInterface} methods of the returned buffers.
*
* @return An iterator on the record buffers of interest. In this
* implementation, the iterator will only return this buffer
* instance.
*/
@Override
public Iterator<RecordBuffer> recordBuffers()
{
if (recordBuffers == null)
{
recordBuffers = Arrays.asList(new RecordBuffer[] { this });
}
return recordBuffers.iterator();
}
/**
* Respond to a record change event. This implementation stores the
* event's DMO snapshot as its own DMO snapshot if the following
* conditions are met:
* <ol>
* <li>this buffer's snapshot has not already been stored previously;
* <li>the event's <i>post-modification</i> DMO instance is the same
* object instance as this buffer's current record;
* <li>the current record was not newly created.
* </ol>
* <p>
* If the event indicates a record delete operation, we check if the
* deleted record is the same as our current record. If so, we clear it
* from the buffer, since that record should no longer be available in any
* buffer, once it has been deleted.
* <p>
* In the special case where a different, local buffer created the record we have loaded,
* and the record is not yet flushed, we will be notified of the flush event here.
* Stop tracking the creating buffer.
*
* @param event
* Event which describes the DMO state change.
*
* @throws PersistenceException
* not thrown; required by the interface.
*/
@Override
public void stateChanged(RecordChangeEvent event)
throws PersistenceException
{
if (!isActive())
{
return;
}
Record dmo = event.getDMO();
if (snapshot == null && dmo == currentRecord && !newlyCreated)
{
if (event.getSnapshot() != null && undoable != null)
{
undoable.checkUndoable(true);
}
snapshot = event.getSnapshot();
}
if (dmo == currentRecord && currentRecord != null)
{
if (event.isDelete())
{
release(false);
}
else if (creatingBuffer != null && event.isInsert() && event.getSource() == creatingBuffer)
{
// the creating buffer has flushed the record to the database; we no longer need to
// delegate setter calls to it
creatingBuffer = null;
}
else if (event.getSource() != this &&
currentRecord.isMultiplyReferenced(false) &&
event.isUpdate())
{
// this is also a dirty buffer if the record was updated using another buffer
bufferManager.registerDirtyBuffer(this);
}
}
if (currentRecord != null && event.isBufferRefresh())
{
Long id = currentRecord.primaryKey();
reload(getPinnedLockType(id), false);
}
}
/**
* Get the entity name associated with this buffer, which is the fully
* qualified name of the DMO implementation class.
*
* @return DMO implementation class name.
*/
public String getEntityName()
{
checkActive();
return dmoClass.getName();
}
/**
* Resets the <code>sortIndex</code> used by this buffer to <code>null</code>.
*/
public void resetSortIndex()
{
this.sortIndex = null;
}
/**
* Get <code>IndexHelper</code> instance used by this buffer.
*
* @return <code>IndexHelper</code> instance.
*/
public IndexHelper getIndexHelper()
{
checkActive();
return indexHelper;
}
/**
* Get a representation of the latest DMO to be held in the buffer. This
* may be the same instance as current record; it may be a copy. In
* cases where the current record has been deleted or released, this
* represents the most recent record to back the buffer <i>before</i> such
* deletion or release.
* <p>
* This is used to enable a query to keep its place when navigating to a
* record, based upon the data in the last record held in the buffer,
* even if that record is no longer available.
*
* @return Snapshot of the latest DMO record, or <code>null</code> if the
* buffer never held a current record.
*/
public Record getSnapshot()
{
return (snapshot != null) ? snapshot : currentRecord;
}
/**
* Notifies that a block is about to be normally exited or normally iterated and that any
* deferred validation processing may need to be executed.
* <p>
* A normal exit or iteration is one that occurs naturally by hitting the end of the block (or
* loop). A <code>LEAVE</code>, <code>NEXT</code> and <code>RETURN</code> are also considered
* normal exits or iterations so long as they were not preceded by an <code>UNDO</code>.
* Likewise, a <code>QUIT</code> is a normal exit so long as it is not inside a block that has
* an <code>ON QUIT UNDO, <action></code>.
* <p>
* If the buffer is active and we are at a full transaction boundary or the boundary of the
* buffer scope, we fire any write triggers that are pending. Also, if the current record is
* transient, i.e., nothing has triggered a flush by the time the creating block is about to
* exit/iterate, we validate and flush the record.
*
* @param transaction
* <code>true</code> if this is a full transaction and <code>false</code> if this is
* only a sub-transaction (a nested scope with transaction support). This method will
* only be invoked outside a transaction for no-undo temp-table buffers.
* @param aggressiveFlush
* <code>true</code> if transaction manager is in aggressive subtransaction
* flush mode, indicating that any transient buffers should be validated and
* flushed, regardless of other state.
*
* @throws ErrorConditionException
* If validation fails.
*/
@Override
public void validate(boolean transaction, boolean aggressiveFlush)
{
if (!isActive() ||
currentRecord == null ||
!inChangeScope() && !isTransient() ||
currentRecord.checkState(DmoState.DELETING))
{
// nothing to do
return;
}
// if a separate buffer created the current record, then it is tracking all change to that
// record and will handle validation and triggers
if (creatingBuffer != null)
{
return;
}
// at a full transaction boundary, flush/validate the record (flush() will determine if it is
// actually needed)
boolean flush = transaction;
if (!flush && (currentRecord.checkState(DmoState.NEW) || currentRecord.checkState(DmoState.CHANGED)))
{
// override aggressive flush if we are processing a CREATE trigger
if (aggressiveFlush &&
isTransient() &&
triggerTracker != null &&
triggerTracker.isExecuting(DatabaseEventType.CREATE, currentRecord.primaryKey()))
{
aggressiveFlush = false;
}
// at a sub-transaction boundary, validate and flush a new or changed record
// Note: for a new record, this means default values will be saved in any fields which have not
// otherwise been explicitly updated
boolean global = registeredWithGlobalScope == 0;
int currentScope = bufferManager.getOpenBufferScopes();
boolean exitingActiveScope = currentScope == activeScopeDepth && (!global || !worldScope);
// if the record is also loaded in another buffer,
// don't force the flushing even if this buffer is running out of scope
if (!isUndoable() ||
(exitingActiveScope && !(currentRecord.isMultiplyReferenced(false))) ||
aggressiveFlush)
{
flush = !isUndoable() && !aggressiveFlush ? transaction ||
!currentRecord.hasIndices() ||
currentRecord.isAnyIndexFullyDirty() :
true;
}
else
{
return;
}
// TODO: need better test for flush/write trigger, to ensure it reflects the LAST buffer
// holding the modified record, rather than ANY buffer holding the modified record
}
if (flush)
{
try
{
flush(true);
}
catch (ValidationException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
}
/**
* Process a commit event for the current buffer.
*
* @param transaction
* {@code true} if this commit represents the master transaction commit;
* {@code false} if it represents a sub-transaction commit.
*/
@Override
public void commit(boolean transaction)
{
if (!isActive())
{
return;
}
if (transaction && isTouched && !firingRowUpdate)
{
firingRowUpdate = true;
maybeFireRowUpdateEvent();
isTouched = false; // prevent firing ROW-UPDATE a second time when record is released
firingRowUpdate = false;
}
int currentScope = bufferManager.getOpenBufferScopes();
if (triggerTracker != null)
{
triggerTracker.commit(currentScope);
}
}
/**
* Process a rollback event for the current buffer.
* <p>
* Auto-commit buffers are never rolled back.
*
* @param transaction
* {@code true} if this commit represents the master transaction rollback;
* {@code false} if it represents a sub-transaction rollback.
*/
@Override
public void rollback(boolean transaction)
{
if (!isActive())
{
return;
}
boolean inChangeScope = inChangeScope();
if (isUndoable())
{
int currentScope = bufferManager.getOpenBufferScopes();
if (triggerTracker != null)
{
triggerTracker.rollback(currentScope);
}
}
else if (inChangeScope && transaction)
{
resetChangeScope();
}
}
/**
* Called by the transaction manager just before a buffer scope closes.
* This is our hook to update the state of locks and record availability.
* The current record is made unavailable and locks acquired during this
* scope are either:
* <ul>
* <li>released immediately, if this buffer scope never intersected a
* transaction's scope;
* <li>downgraded, if a transaction occurred within this buffer scope,
* but ended previous to this call; or
* <li>marked for deferred release, if we are still within the scope of
* an active transaction; the locks will be released at the end of
* the transaction.
* </ul>
*/
public void finished()
{
// need to be sure delayed is not polluted by a previous call
delayed = !isDynamic() && txHelper.isExternalBlock();
if (!delayed)
{
finishedImpl();
}
else
{
// we are a static buffer scoped to the external program (or OE class) and must be world scope
worldScope = true;
}
}
/**
* Provides a notification that the external program scope in which the object is registered is
* being deleted and the object's reference will be lost after this method is called.
*/
@Override
public void deleted()
{
if (delayed)
{
finishedImpl();
}
}
/**
* Called by the transaction manager after each iteration of a repeating
* block. Releases the current record. Relinquishes locks pinned by this
* buffer if we are at the outermost scope boundary.
*/
public final void iterate()
{
boolean active = isActive();
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
describe() + ": iterating " + (active ? "" : "in") + "active buffer scope");
}
if (!active)
{
return;
}
// Release the current record if this behavior has not been overridden
// in the current scope.
// JPRM @34715
int currentScope = bufferManager.getOpenBufferScopes();
if (currentScope != iterateReleaseOverrideScope && isUndoable())
{
// JPRM @30382.
// do not allow write trigger to fire here; we are not at a point in the block
// processing cycle where we can handle arbitrary business logic
release(false);
}
// We are about to make another pass through an iterating block.
// Relinquish any record locks this buffer is pinning, if we are at the
// outermost open scope boundary.
if (currentScope == activeScopeDepth && !bufferManager.isTransaction())
{
Set<Long> released = persistenceContext.getRecordLockContext().relinquishBufferLocks(this);
if (released != null)
{
bufferManager.reclaimPendingKeys(persistence, released);
}
}
}
/**
* Called by the transaction manager before a block is retried after an
* error. Relinquishes locks pinned by this buffer if we are at the
* outermost scope boundary.
*/
public final void retry()
{
// on retry, set a flag so the next openScope call will act accordigly
retrying = true;
boolean active = isActive();
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
describe() + ": retrying " + (active ? "" : "in") + "active buffer scope");
}
if (!active)
{
return;
}
// We are about to retry a block. Relinquish any record locks this
// buffer is pinning, if we are at the outermost open scope boundary.
int currentScope = bufferManager.getOpenBufferScopes();
if (currentScope == activeScopeDepth && !bufferManager.isTransaction())
{
Set<Long> released = persistenceContext.getRecordLockContext().relinquishBufferLocks(this);
if (released != null)
{
bufferManager.reclaimPendingKeys(persistence, released);
}
}
}
/**
* Checks whether the record contained is a virtual system table (VST).
*
* @return {@code true} when the record contained is a VST.
*/
public boolean isVst()
{
return vst;
}
/**
* Return a reference to this object instance. This is implemented to
* satisfy the <code>BufferReference</code> interface, and provides a
* means for the persistence layer to retrieve a reference to the backing
* <code>RecordBuffer</code> instance for a DMO proxy object provided by
* client code. It is invoked by the enclosed invocation handler
* implementation, which services proxied method invocations on DMO proxy
* objects.
*
* @return Current record buffer (i.e., <code>this</code>).
*/
public final RecordBuffer buffer()
{
return this;
}
/**
* Resolve the instance as it exists when the external program or 4GL class was
* initially instantiated.
*
* @return The definition-time instance.
*/
public final RecordBuffer definition()
{
return this;
}
/**
* Get the DMO which represents the database record currently held in the buffer.
*
* @return DMO for the current record, or <code>null</code> if the buffer currently is empty.
*/
public Record getCurrentRecord()
{
return currentRecord;
}
/**
* Get the lock type which potentially "pins" the communal record lock on
* the record with the given identifier (the record was ever or is
* currently loaded into this buffer)
*
* @param id
* Record identifier.
*
* @return Pinned lock type of the specified record.
*
* @see RecordLockContext
*/
public LockType getPinnedLockType(Long id)
{
LockType lockType = pinnedLocks.getPinnedLockType(id);
return (lockType == null ? LockType.NONE : lockType);
}
/**
* Set the lock type which potentially "pins" the communal record lock on
* the record with the given id (the record was ever or is currently loaded
* into this buffer).
*
* @param id
* Record identifier.
* @param pinnedLockType
* New pinned lock type.
*
* @see RecordLockContext
*/
public void setPinnedLockType(Long id, LockType pinnedLockType)
{
pinnedLocks.setPinnedLockType(id, pinnedLockType);
}
/**
* Get the state of the {@link #worldScope} flag.
*
* @return See above.
*/
public boolean isWorldScope()
{
return worldScope;
}
/**
* Get the depth of the scope at which this buffer was first opened.
* Nested scopes are ignored; only the depth of the outermost open scope
* is reported.
*
* @return Depth at which outermost scope was opened for this buffer.
*/
public int getScopeOpenDepth()
{
return activeScopeDepth;
}
/**
* Get the record ID of the current buffer's current record, if any. P2J
* implementation of the RECID function.
* <p>
* This method will return a 32-bit recid which represents the internal
* row ID of the given record. If the actual row ID (which is implemented
* as a wider integer) cannot fit due to overflow, an error condition will
* be raised.
*
* @return Record ID of the underlying data record. If no record currently
* backs the buffer associated with <code>dmo</code>, a recid
* initialized as unknown value is returned.
*
* @throws ErrorConditionException
* if the actual record ID is too wide to fit within a 32-bit
* integer.
*/
public recid recordID()
{
if (isAvailable())
{
Record record = getCurrentRecord();
long id = record.primaryKey();
if (id < Integer.MIN_VALUE || id > Integer.MAX_VALUE)
{
// TODO: don't know what 4GL's overflow behavior is or whether the corresponding
// error is more serious than an ERROR condition; internally, we support a 64-bit
// value, but the recid object follows the 32-bit constraint in terms of not
// formatting this
txHelper.errHlp.noRecordOrThrowError(
"Row id of " + id + " is too wide to fit within a 32-bit record ID");
}
return new recid(id);
}
else
{
return new recid();
}
}
/**
* Get the row ID of the current buffer's current record, if any. P2J
* implementation of the ROWID function.
*
* @return Row ID of the underlying data record. If no record currently
* backs the buffer associated with <code>dmo</code>, a
* <code>rowid</code> initialized as unknown value is returned.
*/
public rowid rowID()
{
return new rowid(_rowID());
}
/**
* Get the row ID of the current buffer's current record, if any.
*
* @return Row ID of the underlying data record. If no record currently backs the buffer associated with
* <code>dmo</code>, a <code>null</code> value is returned.
*/
public Long _rowID()
{
if (isAvailable())
{
Record record = getCurrentRecord();
Long id = record.primaryKey();
if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && id == null)
{
ErrorManager.recordOrShowError(15991, getLegacyName());
// Attempt to create in multi-tenant table '<tablename>' where the partition (usually default) has not been allocated. (15991)
return null;
}
return id;
}
return null;
}
/**
* Returns TABLE-HANDLE for the buffer.
*
* @return TABLE-HANDLE for the buffer. Is an unknown handle for permanent tables.
*/
public handle tableHandle()
{
return new handle();
}
/**
* Get the associated temp-table (for tempporary buffers).
*
* @return Always <code>null</code>.
*/
public TempTable tableHandleResource()
{
return null;
}
/**
* Get the DMO interface associated with this buffer.
*
* @return DMO interface.
*/
public Class<? extends DataModelObject> getDMOInterface()
{
return dmoInterface;
}
/**
* Get the meta information of the DMO associated with the buffer.
*
* @return the meta information collection of the DMO associated with the buffer.
*/
public DmoMeta getDmoInfo()
{
return dmoInfo;
}
/**
* Validate the current record and flush it to the database, if validation passes. However, abort
* if the record already is marked invalid or is stale.
*
* @throws ValidationException
* if validation on the buffer fails.
* @throws ErrorConditionException
* if there is an error accessing the database while persisting the record.
*/
public void flush()
throws ValidationException
{
flush(false);
}
/**
* Validate the current record and flush it to the database, if validation passes. Abort if the record
* is stale.
*
* @param retryInvalid
* if {@code true}, retry the validation and flush even if the record previously was marked as
* invalid; if {@code false}, abort if the record is already marked as invalid.
*
* @throws ValidationException
* if validation on the buffer fails.
* @throws ErrorConditionException
* if there is an error accessing the database while persisting the record.
*/
private void flush(boolean retryInvalid)
throws ValidationException
{
if (readonly)
{
// the read-only buffers (those used as old values in WRITE triggers) must not be altered so
// no need to be flushed to database
return;
}
// let the transaction Id to be generated if it was not yet set:
getTxHelper().checkTransaction(persistence.getDatabase(!dmoInfo.multiTenant).getId());
if (vst)
{
checkReadOnlyVstBuffer();
return;
}
// if no current record, OR the record has been marked STALE, OR the record is in the process of being
// deleted, OR the record already has been determined to be invalid AND we are not ignoring that fact,
// abort the flush
if (currentRecord == null ||
currentRecord.checkState(DmoState.STALE) ||
currentRecord.checkState(DmoState.DELETING) ||
(currentRecord.checkState(DmoState.INVALID) && !retryInvalid))
{
return;
}
try
{
// validate and persist the record
boolean changed = currentRecord.checkState(DmoState.CHANGED);
if (changed || currentRecord.checkState(DmoState.NEW))
{
validateMaybeFlush(currentRecord, true, false);
if (changed)
{
UserTableStatUpdater.update(getDatabase(), this);
}
}
else if (currentRecord.checkState(DmoState.INVALID))
{
throw new ValidationException("Invalid record.");
}
else
{
// record was neither in need of validation nor invalid, so there is nothing more to do
return;
}
/*
// share uncommitted change to the record, if not previously done
if (dirtyContext != null && dirtyShared)
{
dirtyContext.insert(this, currentRecord, null, true);
}
*/
if (LOG.isLoggable(Level.FINE))
{
String desc = describe();
LOG.log(Level.FINE, desc + ": flushed buffer");
if (LOG.isLoggable(Level.FINEST))
{
LOG.log(Level.FINEST, desc + ": " + toString());
LOG.log(Level.FINEST, desc + ": location:", new Throwable());
}
}
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
/**
* Get code page of the specified CLOB field.
*
* @param property
* DMO property name of the CLOB field.
*
* @return code page of the specified CLOB field.
*/
public String getCodePage(String property)
{
// for permanent buffers only, overridden for temporary buffers
return TableMapper.getLegacyFieldInfo(dmoInfo, property).getCodePage();
}
/**
* Checks whether the table is read-only. If the current DMO belongs to a (VST) table that does
* not allow the modified data to be persisted then the flush event should be discarded. Note
* that some VST tables will silently discard the flush event while some of them will also
* display the 14378 error message.
*/
private void checkReadOnlyVstBuffer()
{
String legacyName = dmoInfo.legacyTable;
if (isTouched)
{
String msg = "Updating " + legacyName + " Virtual System Table is not supported";
ErrorManager.recordOrShowError(14378, msg, true);
}
if (legacyName.equalsIgnoreCase("_Trans"))
{
// flush the transaction id to persistence if required
TransactionManager.flushTransMetaData(getPersistence());
}
}
/**
* Return the hash code for this object. Overrides the parent's behavior to return a value
* which is cached at construction and does not change. Implemented as a performance
* enhancement.
*
* @return Hash code.
*/
@Override
public final int hashCode()
{
return hashCode;
}
/**
* Determine whether this object is equivalent to the specified object. Forces the parent's
* implementation.
*
* @param o
* Object to test for equality.
*
* @return {@code true} if equivalent, else {@code false}.
*/
@Override
public final boolean equals(Object o)
{
return super.equals(o);
}
/**
* Get a string representation of the internal state of this object,
* primarily for debugging purposes.
*
* @return String representing this object's state.
*/
public String toString()
{
Record tempRecord = getTempRecord();
if (tempRecord != null)
{
return "[TEMP RECORD ACTIVE]\n" + toString(tempRecord);
}
return toString(currentRecord);
}
/**
* Constructs and returns a string containing the P4GL schema definition of the table. The result is not
* cached so this method is to be used only for debug.
*
* @return a string containing the P4GL legacy schema definition of the table.
*/
public String tableDefinition()
{
return dmoInfo.getTableDefinition();
}
/**
* Get the DMO proxy which references this buffer instance.
*
* @return DMO proxy object. This is {@code null} only if this object is incompletely constructed.
*/
public BufferReference getDMOProxy()
{
return dmoProxy;
}
/**
* Get parent table.
*
* @return Parent <code>TempTable</code> object for temporary buffers or <code>null</code>
* for permanent buffers.
*/
public AbstractTempTable getParentTable()
{
TempTable tt = tableHandleResource();
return tt != null && tt.valid() ? (AbstractTempTable) tt : null;
}
/**
* Get the zero-based transaction block nesting level, where 0 indicates the current block is a full
* transaction block, 1 indicates the current block is the first level of nested subtransaction, and so
* on. Note that this includes only blocks with transaction properties; no-transaction blocks which may
* be nested between blocks with transaction properties or ignored for purposes of this count.
*
* @return The current block's transaction nesting level, or -1 if not currently in an active
* transaction.
*/
public Long getTxNestingId()
{
Session session = persistenceContext.getSessionNoCreate();
return session != null ? session.getTxNestingId() : null;
}
/**
* Get the currently active ORM database session, if any.
*
* @return Current database session, or {@code null} if none is active.
*/
public Session getSession()
{
return getSession(false);
}
/**
* Get the currently active ORM database session, optionally creating one if it does not exist.
*
* @param create
* If {@code true} and there is no current database session, create one. Otherwise, return the
* current session, even if it is {@code null}.
*
* @return Current database session or {@code null}.
*/
public Session getSession(boolean create)
{
if (!create)
{
return persistenceContext.getSessionNoCreate();
}
try
{
return persistenceContext.getSession();
}
catch (PersistenceException exc)
{
DBUtils.handleException(getDatabase(), exc);
return null;
}
}
/**
* Initialize the core data on which this buffer relies. This should be
* called only once per buffer instance, when the buffer's scope is first
* opened.
*
* @return {@code true} if the buffer was initialized with this call;
* {@code false} if the buffer had been initialized with a
* previous call to this method.
*
* @throws IllegalArgumentException
* if no implementation class is found for the given database and
* interface.
* @throws RuntimeException
* if the backing DMO implementation class does not implement a
* constructor which accepts a multiplex ID.
*/
public boolean initialize()
{
return initialize(false);
}
/**
* Get the id of the tenant (if any) from the persistence context.
*
* @return The id of the tenant from the persistence context.
*/
public String getTenantName()
{
return persistenceContext.getTenantName();
}
/**
* Tenant changes forces update of (cached) tenant-related internal data.
*
* @param context
* The context containing the updated tenant info.
*/
void updateTenant(Persistence.Context context)
{
persistenceContext = context;
}
/**
* Initialize the core data on which this buffer relies. This should be called after the
* buffer's scope is first opened, but before the buffer is used for a query, or interrogated
* for any information besides its DMO interface or variable name.
*
* @param fromDefine
* {@code true} to acknowledge that this is an early initialization from a define statement
* and the scope shouldn't have been opened before-hand.
*
* @return {@code true} if the buffer was initialized with this call;
* {@code false} if the buffer had been initialized with a
* previous call to this method.
*
* @throws IllegalArgumentException
* if no implementation class is found for the given database and interface; if setup
* for an invalid foreign relation fails.
* @throws StopConditionException
* if index information cannot be obtained for backing table or if database isn't
* connected.
*/
public boolean initialize(boolean fromDefine)
{
if (persistence != null)
{
if (bufferManager.isTransaction())
{
bufferManager.maybeActivateTxWrapper(persistence.getDatabase(!dmoInfo.multiTenant));
}
return false;
}
Database database = getDatabase();
if (database == null)
{
// error will have been processed by getDatabase method
return false;
}
return initialize(fromDefine, PersistenceFactory.getInstance(database), database);
}
/**
* Initialize the core data on which this buffer relies. This should be called after the
* buffer's scope is first opened, but before the buffer is used for a query, or interrogated
* for any information besides its DMO interface or variable name.
*
* @param fromDefine
* {@code true} to acknowledge that this is an early initialization from a define statement
* and the scope shouldn't have been opened before-hand.
* @param p
* The Persistence instance used by this buffer.
* @param database
* The Database instance used by this buffer.
*
* @return {@code true} if the buffer was initialized with this call;
* {@code false} if the buffer had been initialized with a
* previous call to this method.
*
* @throws IllegalArgumentException
* if no implementation class is found for the given database and interface; if setup
* for an invalid foreign relation fails.
* @throws StopConditionException
* if index information cannot be obtained for backing table or if database isn't
* connected.
*/
private boolean initialize(boolean fromDefine, Persistence p, Database database)
{
BufferManager.registerScopeable(bufferManager);
// Get IndexHelper for this database.
indexHelper = IndexHelper.get(database);
// Get Persistence instance used by this buffer.
persistence = p;
persistenceContext = persistence.getContext(!dmoInfo.multiTenant);
// get dirty share context used by this buffer. If the table is permanent and not marked with read-dirty
// annotation, it is categorized to PRIMARY_NON_DIRTY database from POV of dirty context.
Database shareCtxDb = dmoInfo.isTempTable() || dmoInfo.isDirtyRead()
? database
: new Database(database.getName(), Database.Type.PRIMARY_NON_DIRTY);
dirtyContext = DirtyShareFactory.getContextInstance(shareCtxDb);
// Generate BufferType, which will be used for a hash key with Reversibles.
bufferType = new BufferType(database, getTable(), isUndoable());
metadata = dmoInfo.getRecordMeta();
// Notify the connection manager that this buffer is using the backing, physical database.
onOpenOutermostScope();
// Store synchers for all foreign key associations from this buffer's
// table to other tables, and the inverse associations from other
// tables to this buffer's table.
if (Persistence.isForeignKeysEnabled())
{
Iterator<RelationInfo> iter1 = dmoInfo.inverseRelations();
Iterator<RelationInfo> iter2 = dmoInfo.relations();
assocSynchers = (iter1.hasNext() || iter2.hasNext() ? new ArrayList<>() : null);
if (assocSynchers != null)
{
// Create inverse foreign synchers.
while (iter1.hasNext())
{
RelationInfo next = iter1.next();
assocSynchers.add(new InverseSyncher(this, next));
}
// Create primary foreign synchers.
while (iter2.hasNext())
{
RelationInfo next = iter2.next();
assocSynchers.add(new LocalSyncher(this, next));
}
assocSynchers.trimToSize();
}
legacyForeignKeys = new HashSet<>();
// dirtyLegacyKeyProps = new HashSet<>();
/*
// Store synchers for all foreign key associations from this buffer's
// table to other tables.
String schema = getSchema();
Iterator<RelationInfo> iter = DmoMetadataManager.relations(schema, dmoInterface);
assocSynchers = (iter.hasNext()
? new ArrayList<AssociationSyncher>()
: null);
if (assocSynchers != null)
{
// Create primary association synchers.
while (iter.hasNext())
{
RelationInfo next = iter.next();
assocSynchers.add(new LocalSyncher(this, next));
}
assocSynchers.trimToSize();
}
*/
}
else
{
assocSynchers = null;
legacyForeignKeys = null;
}
if (!bufferManager.bufferInitialized(this) && !fromDefine)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "FAIL-SAFE: The buffer is initialized without having its scope opened. "
+ "Registering it as finalizable now! Tracing info: { "
+ "DMOBufIface: " + this.getDMOBufInterface().getName() + ", "
+ "definingType: " + this.getDefiningType() + ", "
+ "variable: " + this.variable + " }");
}
// this is a fallback plan required to ensure closeBufferScope is called to avoid memory leaks
txHelper.registerFinalizable(this, false);
}
if (bufferManager.isTransaction())
{
bufferManager.maybeActivateTxWrapper(database);
}
this.reversibleOffend = false;
return true;
}
/**
* Indicate whether the buffer currently contains a record, and whether
* that record is transient. A record is transient if it has been newly
* created and has not yet been saved to the database.
*
* @return <code>true</code> if record is new, else <code>false</code>.
*/
public boolean isTransient()
{
return (currentRecord != null && createScope >= 0);
}
/**
* Indicate whether this is a temp-table buffer and we are not currently within an existing
* database transaction.
*
* @return {@code false} for permanent table buffers; subclasses should override for
* different behavior.
*/
public boolean isAutoCommit()
{
return false;
}
/**
* Returns the persistent procedure which is responsible for this record buffer.
* This means that this persistent procedure was the first one to open meaning
* that this is a global buffer for the procedure.
*
* @return The persistent procedure which defined this record buffer for the first time.
*/
public Object getPersistentProc()
{
return persistentProc;
}
/**
* Convenience method to indicate whether this buffer is backed by a
* temporary table.
*
* @return <code>false</code>.
*/
public boolean isTemporary()
{
return false;
}
/**
* Check whether this buffer is associated with a persistent table from a mutable database (i.e., that
* its DMO is generated at runtime by a {@link SchemaCheck database scan}).
* <p>
* TODO: this only checks whether the backing database is configured as mutable; the current assumption
* is that all tables in a mutable database are mutable, but in the future, we may need to support mutable
* databases with a mix of static and dynamic tables.
*/
public boolean isMutable()
{
try
{
Database database = getDatabase();
if (database.isMeta())
{
database = database.toType(Database.Type.PRIMARY);
}
return DatabaseManager.isMutable(database);
}
catch (PersistenceException exc)
{
return false;
}
}
/**
* Check whether this instance was defined as REFERENCE-ONLY. This applies only to temp-tables so this
* method returns {@code false} all the time.
*
* @return always {@code false}.
*/
public boolean referenceOnly()
{
return false;
}
/**
* Convenience method to get the {@code Dialect} in use for this record buffer.
*
* @return Database dialect.
*/
public Dialect getDialect()
{
checkActive();
return persistence.getDialect();
}
/**
* Invalidates all or a specific index for this buffer's table.
*
* @param index
* If not null, it represents the index to be invalidated in ff cache. Otherwise all indices
* are invalidated.
*/
public void invalidateFFCache(int index)
{
FastFindCache ffCache = persistenceContext.getFastFindCache();
if (index == 0)
{
// invalidate all indices
ffCache.invalidate(dmoInfo.getId(), getMultiplexID());
}
else
{
// invalidate all only the specified one
ffCache.invalidate(dmoInfo.getId(), getMultiplexID(), index);
}
}
/**
* Notification that the record in this buffer has been flushed.
*/
public void recordFlushed()
{
bufferManager.getTxWrapperHelper().recordFlushed(this, metadata.getDmoMeta().getId());
}
/**
* Use this method to notify this object that the record it holds was successfully inserted into the
* database. As result, the record is removed from {@code DirtyShareContext.earlyInserts}.
*
*/
public void recordInserted()
{
if (dirtyContext != null && currentRecord != null)
{
dirtyContext.recordInserted(new RecordIdentifier<>(getEntityName(),
currentRecord.primaryKey()));
}
}
/**
* Get the buffer's defining type.
*
* @return The {@link #definingType}.
*/
protected Class<?> getDefiningType()
{
return definingType;
}
/**
* Prune the session cache of all records associated with this buffer's backing table.
*/
protected void pruneSessionCache()
{
Session session = getSession(false);
if (session != null)
{
session.invalidateRecords(getDMOImplementationClass().getName(), getMultiplexID());
}
}
/**
* Apply the given update to all the cached records.
* <p>
* WARNING: this must be called only for temp-tables, and it must be used with a direct SQL on the same
* temp-table records, so that the cached and the sql table records have the same state.
*
* @param work
* The code to apply on the cached records.
*/
protected void updateCachedRecords(Consumer<BaseRecord> work)
{
Session session = getSession(false);
if (session != null)
{
session.updateCachedRecords(getDMOImplementationClass().getName(), getMultiplexID(), work);
}
}
/**
* Gets the current value of the {@code ROW-STATE} property for this record. Valid values
* are defined as constants in {@link Buffer} interface. Additionally, {@code unknown} value is
* returned if property is not available.
*
* @return Since this method is invoked for permanent records this will always return
* {@code unknown} value.
*/
protected integer rowState()
{
// not a temp-table record
return new integer();
}
/**
* Sets the value of the {@code ROW-STATUS} property for this record. Valid values are defined
* as constants in {@link Buffer} interface. Additionally {@code null} value is allowed if
* property is not available.
*
* @param state
* The new value of the {@code ROW-STATE} property for this record as explained above.
*/
protected void rowState(Integer state)
{
// not a temp-table record
}
/**
* Gets {@code rowid} of the peer record. Exclusively, for a BEFORE-TABLE record this is the
* {@code after-rowid} and {@code before-rowid} for AFTER-TABLE record.
*
* @return Since this method is invoked for permanent records this will always return
* {@code unknown} value.
*/
protected rowid peerRowid()
{
// not a temp-table record
return new rowid();
}
/**
* Sets the {@code rowid} value of the peer record. Exclusively, for a BEFORE-TABLE record this
* is the {@code after-rowid} and {@code before-rowid} for AFTER-TABLE record.
*
* @param peer
* The new rowid value of the peer record or {@code null}.
*/
protected void peerRowid(rowid peer)
{
// not a temp-table record
}
/**
* Sets the {@code origin-rowid} for AFTER-TABLE record.
*
* @param peer
* The new rowid value of the record or {@code null}.
*/
public void originRowid(rowid peer)
{
// not a temp-table record
}
/**
* Sets the {@code datasource-rowid} for AFTER-TABLE record.
*
* @param sourceRowid
* The new long rowid value of the record or {@code null}.
*/
public void datasourceRowid(rowid sourceRowid)
{
// not a temp-table record
}
/**
* Gets {@code errorFlag} of this record.
*
* @return the {@code errorFlag} of this record or {@code null} if it was not configured. If not null,
* the value is a bitwise combination of ERROR and REJECTED attribute. It's up to the called to
* extract the bit(s) it needs.<br>
* Since this method is invoked for permanent records this will always return {@code unknown}
* value.
*/
protected Integer errorFlags()
{
// not a temp-table record
return null;
}
/**
* Sets or removes the {@code errorFlag} value this peer record. The {@code error} attribute of the record
* is composed of multiple bit flags. This method manages its value based on the parameters it receives.
*
* @param errFlag
* The error bit to be set or removed.
* @param set
* Use {@code true} to set the flag and {@code false} to remove it.
*/
protected void updateErrorFlags(int errFlag, boolean set)
{
// not a temp-table record
}
/**
* Gets {@code error string} of this record.
*
* @return Since this method is invoked for permanent records this will always return
* {@code unknown} value.
*/
protected character errorString()
{
// not a temp-table record
return new character();
}
/**
* Sets the {@code error string} of this record.
*
* @param string
* The new {@code error string} of this record.
*/
protected void errorString(Text string)
{
// not a temp-table record
}
/**
* Potentially set the current high-water block depth at which an undoable change has been
* made.
*/
protected final void markChangeScope()
{
MutableInteger changeScope = bufferManager.changeScopes.get(bufferType);
int currentScope = bufferManager.getOpenBufferScopes();
if (changeScope != null)
{
changeScope.set(Math.max(changeScope.get(), currentScope));
}
else
{
bufferManager.changeScopes.put(bufferType, new MutableInteger(currentScope));
}
}
/**
* Reset to its default state the current high-water mark block depth at which an undoable
* change has been made.
*/
protected final void resetChangeScope()
{
bufferManager.changeScopes.remove(bufferType);
}
/**
* Determine whether the current open buffer scope is at or lower than the current high-water mark
* open buffer scope depth at which an undoable change has been made. If it is not, we can be confident
* that there are not reversible actions stored, thereby bypassing the effort to manage any
* such resources at full and sub-transaction boundaries.
*
* @return {@code true} if the current open scope depth is within the scope of any reversible
* changes that need to be managed; else {@code false}.
*/
protected final boolean inChangeScope()
{
MutableInteger changeScope = bufferManager.changeScopes.get(bufferType);
int currentScope = bufferManager.getOpenBufferScopes();
return changeScope != null && currentScope <= changeScope.get();
}
/**
* Get default buffer (for temporary tables only).
*
* @return for temporary tables: default buffer for the table to which this buffer belongs. For
* permanent tables <code>null</code> is returned.
*/
protected RecordBuffer getDefaultBuffer()
{
if (!isTemporary)
{
return null;
}
TempTable table = getParentTable();
Buffer buf = table.defaultBufferHandleNative();
return get((DataModelObject) buf);
}
/**
* Close related queries.
*/
protected void closeRelatedQueries()
{
if (relatedQueries != null && !relatedQueries.isEmpty())
{
P2JQuery[] queries = relatedQueries.toArray(new P2JQuery[relatedQueries.size()]);
for (P2JQuery query : queries)
{
if (query.isOpen().booleanValue())
{
query.close();
}
query.resetQuery();
}
}
}
/**
* A no-op implementation called during scope cleanup ({@link #finishedImpl()}), which should
* be overridden by a subclass needing to perform some action at this point.
*/
protected void resourceDeleted()
{
// remove buffer from BufferManager's tracking
bufferManager.deregisterDynamicBuffer(this, persistentProc);
// remove buffer from TransactionManager's global finalizables, unless we are being
// invoked in the global scope, since we will be removed imminently anyway
int currentScope = bufferManager.getOpenBufferScopes();
if (currentScope > 0)
{
txHelper.deregisterGlobalFinalizable(this);
}
// release the persistent procedure reference.
this.persistentProc = null;
}
/**
* Get the state of the {@link #isTouched} flag.
*
* @return See above.
*/
protected boolean isTouched()
{
return isTouched;
}
/**
* Get the {@link #bulkCopy} flag.
*
* @return See {@link #bulkCopy}.
*/
protected boolean isBulkCopy()
{
return bulkCopy;
}
/**
* Set the {@link #bulkCopy} flag.
*
* @param bulk
* <code>true</code> if buffer currently the destination of a bulk copy operation.
*/
protected void setBulkCopy(boolean bulk)
{
this.bulkCopy = bulk;
}
/**
* Indicate whether changes made to records managed by this buffer can be
* undone during a rollback. This implementation always returns
* <code>true</code>.
*
* @return <code>true</code>.
*/
protected boolean isUndoable()
{
return true;
}
/**
* Indicate whether the table backing this buffer is known to either: (a) not exist; or (b) contain no
* rows.
*
* @return {@code false} to indicate it is not definitively known whether the backing table exists or
* might have rows.
*/
protected boolean isTableDefinitelyEmpty()
{
return false;
}
/**
* Indicate whether the table backing this buffer contains records. If this can't be inferred easily,
* this returns null.
*
* @return {@code true} if the table surely has records, {@code false} if the table surely doesn't have
* records, {@code null} if this can't be computed wihout actually querying the database.
*/
public Boolean fastHasRecords()
{
return null;
}
/**
* Get the logical database name associated with the given parameter. If the parameter already
* represents a logical database name, this name is returned. If it represents a valid alias,
* the underlying, logical name is returned.
*
* @param ldbOrAlias
* A logical database name or database alias.
*
* @return Logical database name or {@code null} if {@code ldbOrAlias} is not a valid and
* connected logical database name or alias.
*/
protected String getLDBName(String ldbOrAlias)
{
ConnectionManager cm = bufferManager.getConnMgr();
// check if there is such a database connected
Database db = cm.getDatabase(ldbOrAlias);
if (db == null)
{
return null;
}
// retrieve logical database name in case this is an alias.
return cm.getLDBName(ldbOrAlias);
}
/**
* Get the physical database name associated with the given logical name.
*
* @param ldbName
* A logical database name.
*
* @return Physical database name.
*/
protected String getPDBName(String ldbName)
{
ConnectionManager cm = bufferManager.getConnMgr();
// Convert logical database name to a physical database name.
return cm.getPDBName(ldbName, true);
}
/**
* Get the database information object associated with the given logical database name.
*
* @param ldbName
* A logical database name.
*
* @return Database information object associated with <code>ldbName</code>.
*/
protected Database getPDB(String ldbName)
{
// Look up physical database by logical database name.
Database db = bufferManager.getConnMgr().getDatabase(ldbName);
return dmoInfo.multiTenant ? db : db.getDefault();
}
/**
* Perform any actions needed when this buffer's outermost scope opens (or if the buffer was not active
* when that scope was opened, when the buffer is first activated thereafter).
*/
protected void onOpenOutermostScope()
{
bufferManager.getConnMgr().openBufferScope(this);
}
/**
* Very specific method to check if a buffer is still dangling in {@link ConnectionManager}.
* This should be called only in extra-ordinary execution paths that may involve
* native Java integrations outside the persist framework. This should be called in case of
* fail-safes to avoid memory leaks.
* <p>
* DON'T use this method for trivial checks on whether the buffer has its scope opened or not.
* This should be used only to detect unexpected leaks inside {@link ConnectionManager} that can't
* be solved by the normal lifecycle of {@link TransactionManager} or {@link BufferManager}.
* <p>
* For example, invoking generic triggers that do not properly manage the buffer lifecycle
* may result in memory leaks. Therefore, attempt to detect this case and improve the fault
* tolerance on persistence layer side.
*
* @return {@code true} if this buffer still resides in {@link ConnectionManager} due to a
* bad handling of the buffer from native Java integrations like generic triggers.
*/
public boolean isBufferScopeLeaking()
{
return bufferManager.getConnMgr().isBufferScopeLeaking(this);
}
/**
* Perform any actions needed when this buffer's outermost scope closes.
*/
protected void onCloseOutermostScope()
{
bufferManager.getConnMgr().closeBufferScope(this);
}
/**
* Create a new instance of the DMO implementation class associated with
* this buffer. All default data values are assigned, but the new instance
* is not yet associated with the current database session.
*
* @return A transient DMO instance.
*
* @throws InstantiationException
* if the class cannot be instantiated.
* @throws IllegalAccessException
* if the DMO class' target constructor cannot be accessed.
* @throws InvocationTargetException
* if the underlying constructor throws an exception.
*/
protected Record instantiateDMO()
throws ReflectiveOperationException
{
return getDMOImplementationClass().getDeclaredConstructor().newInstance();
}
/**
* Open a new buffer scope for this record buffer. This ensures that the
* {@link BufferManager} is tracking this buffer for block scope entries
* and exits within transactions, such that this buffer can be registered
* with the <code>TransactionManager</code> for commit/rollback
* notifications. The buffer is also registered for finish processing so
* that the appropriate record and lock transition processing can occur
* when the buffer scope closes.
*/
protected void openScope()
{
BufferManager.registerScopeable(bufferManager);
if (retrying)
{
// reset the flag
retrying = false;
// this is a retry, nothing to do here
return;
}
this.reversibleOffend = false;
boolean outermost = (openScopeCount == 0);
if (outermost)
{
offEnd = OffEnd.NONE;
// If this is the outermost scope open for this buffer, remember the
// scope depth at which it was opened. This is tracked so that we
// know when it is safe to release resources.
activeScopeDepth = bufferManager.getOpenBufferScopes();
worldScope = false;
// listen for changes driven by this or other buffers for the same DMO type
boolean persistent = pm.isThisProcedurePersistent();
if (persistent && txHelper.getNearestTopLevelType() == BlockType.EXTERNAL_PROC)
{
// the listener is pushed to global scope for persistent procedures; it will be
// removed together with the persistent procedure
changeBroker.addListener(this, 0);
registeredWithGlobalScope = openScopeCount; // save the scope it was globally added
}
else
{
// The listener is added at current scope
changeBroker.addListener(this);
}
if (persistent && persistentProc == null)
{
persistentProc = pm._thisProcedure();
}
// register lightweight undoable to reset current record for undo and retry; this is only for
// undoable tables.
if (isUndoable())
{
txHelper.registerCurrent(this.undoable = new LightweightUndoable(isGlobal()));
}
}
// register this open buffer with the buffer manager; this must be done after the persistent procedure
// logic above, so the buffer manager can determine the right scope at which to track the buffer
bufferManager.openScope(this);
boolean active = isActive();
if (active)
{
onOpenOutermostScope();
}
// Register this buffer as a Finalizable with the TM, so that its
// finished method is called when this buffer scope is about to close.
txHelper.registerFinalizable(this, false);
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
describe() +
": opened " +
(active ? "" : "in") +
"active buffer scope " +
bufferManager.getOpenBufferScopes());
}
// Increment reference count of open scopes for this buffer.
openScopeCount++;
}
/**
* Open a new buffer scope for this record buffer at the specified block depth. This ensures
* that the {@link BufferManager} is tracking this buffer for block scope entries and exits
* within transactions, such that this buffer can be registered with the
* <code>TransactionManager</code> for commit/rollback notifications. The buffer is also
* registered for finish processing so that the appropriate record and lock transition
* processing can occur when the buffer scope closes.
*
* @param blockDepth
* Zero-based depth of the block (starting from the outermost scope) at which the buffer
* scope is opened.
*/
protected void openScopeAt(int blockDepth)
{
BufferManager.registerScopeable(bufferManager);
// Register this scope with the buffer manager.
bufferManager.openScopeAt(blockDepth, this);
activeScopeDepth = blockDepth;
worldScope = false;
// Listen for changes driven by this or other buffers for the same DMO type.
changeBroker.addListener(this, blockDepth);
if (blockDepth == 0 && isUndoable())
{
txHelper.registerCurrent(this.undoable = new LightweightUndoable(isGlobal()));
}
boolean active = isActive();
if (active)
{
onOpenOutermostScope();
}
// Register this buffer as a Finalizable with the TM, so that its
// finished method is called when this buffer scope is about to close.
txHelper.registerFinalizableAt(blockDepth, this);
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
describe() + ": opened " + (active ? "" : "in") + "active buffer scope " + blockDepth);
}
// Increment reference count of open scopes for this buffer.
openScopeCount++;
}
/**
* Get the multiplex ID of this buffer. This default implementation
* always returns <code>null</code>, but subclasses may return a
* non-<code>null</code> value.
*
* @return <code>null</code>.
*/
public Integer getMultiplexID()
{
return null;
}
/**
* Indicate whether the backing table for this buffer is multiplexed; that
* is, whether the backing table represents multiple, virtual tables by
* using a multiplex ID to differentiate table segments.
* <p>
* This default implementation always returns <code>false</code> and should
* be overridden by subclasses which require a different return value.
*
* @return <code>false</code>.
*/
protected boolean isMultiplexed()
{
return false;
}
/**
* Indicate whether this buffer implementation allows bulk actions.
*
* @return <code>false</code>.
*/
protected boolean allowsBulkActions()
{
return false;
}
/**
* Retrieve the master record buffer for this buffer.
* <p>
* This only is meaningful for shared temp tables and should be overridden
* by subclasses.
*
* @return This buffer.
*/
protected RecordBuffer getMasterBuffer()
{
return this;
}
/**
* Fire write triggers, if any are pending, using the record currently in the buffer as the
* current value, and a snapshot of that record taken when it was first changed as the old
* value.
*
* @return {@code true} if the associated trigger might have been executed.
*/
public boolean maybeFireWriteTrigger()
{
if (currentRecord == null || triggerTracker == null) // i.e. not available
{
return false;
}
DatabaseEventType det = DatabaseEventType.WRITE;
Record oldTriggerDMO = triggerTracker.getOldDMO();
Long recId = currentRecord.primaryKey();
// determine whether there's anything to do
if (oldTriggerDMO == null ||
!triggerTracker.isTriggerEnabled(det, null, logicalDatabase) ||
triggerTracker.isExecuting(det, recId))
{
return false;
}
try
{
triggerTracker.pushEvent(det, recId);
openScopeCount++;
DatabaseTriggerManager.trigger(det,
dmoBufInterface,
(Buffer) dmoProxy,
oldTriggerDMO,
logicalDatabase);
}
finally
{
openScopeCount--;
triggerTracker.popEvent(recId);
triggerTracker.resetOldDMO();
}
return true;
}
/**
* Delete the current record from the database. Set the current record to
* null if the deletion is successful.
*
* @throws PersistenceException
* if no record currently is loaded in the buffer, or if there is
* an error deleting the current record from the database.
* @throws ErrorConditionException
* if the database trigger or the validation will fail
*/
protected void delete()
throws PersistenceException
{
// by default there is no validation
delete(() -> logical.TRUE, () -> character.EMPTY_STRING);
}
/**
* Delete the current record from the database. Set the current record to {@code null} if the deletion is
* successful. Reclaim the record's primary key for later re-use.
*
* @param valexp
* The validation expression to evaluate.
* @param valmsg
* The error message to display when validation fails.
*
* @return {@code true} if operation is successful and {@code false} otherwise.
*
* @throws PersistenceException
* if no record currently is loaded in the buffer, or if there is an error deleting the current
* record from the database.
*/
protected boolean delete(Supplier<logical> valexp, Supplier<character> valmsg)
throws PersistenceException
{
if (vst || readonly)
{
// TODO: find the proper message to throw when attempting to delete a record from a readonly table.
// this is quite fatal, NO-ERROR will not prevent it from stopping the procedure
ErrorManager.throwError(6311, "Deleting Virtual System Tables is not allowed", null, false);
return false;
}
if (currentRecord == null)
{
// Nothing to delete!
throw new PersistenceException(
"There is no current " + getLegacyName() + " record. Delete failed",
341);
}
if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && currentRecord.primaryKey() == null)
{
setCurrentRecord(null, true, false, false, false, false);
resetSnapshot();
return true; // the records created by the default tenant are volatile, non persistable
}
Record deletedRecord = currentRecord;
// prevent nested deletes (can be caused by cascading deletes in triggers, due to lack of
// referential integrity in legacy environment)
if (currentRecord.checkState(DmoState.DELETING))
{
return false;
}
bufferManager.registerDirtyBuffer(this);
Session session = persistenceContext.getSessionNoCreate();
try
{
currentRecord.updateState(session, DmoState.DELETING, true);
DatabaseEventType det = DatabaseEventType.DELETE;
if (triggerTracker != null && triggerTracker.isTriggerEnabled(det, null, logicalDatabase))
{
Long recId = currentRecord.primaryKey();
DatabaseEventType topEvent = triggerTracker.peekEvent(recId);
if (topEvent != null)
{
// cannot fire DELETE trigger while any other trigger is executing
DatabaseTriggerManager.handleError(3169, topEvent, dmoInfo.legacyTable, null);
}
else
{
// invoke database triggers
ErrorManager.ErrorHelper err = ErrorManager.getErrorHelper();
boolean wasSilent = err.isSilent();
if (!wasSilent)
{
err.setSilent(true);
}
try
{
triggerTracker.pushEvent(det, recId);
if (!DatabaseTriggerManager.trigger(det,
dmoBufInterface,
(Buffer) dmoProxy,
null,
logicalDatabase))
{
return false; // trigger vetoed, return, no apply
}
}
finally
{
triggerTracker.popEvent(recId);
if (!wasSilent)
{
err.setSilent(false);
}
}
}
}
// only if the trigger succeeds, the validation is tried out
validate(valexp, valmsg);
// the buffer is deleted only if the trigger and the validation succeed
upgradeLock();
// Null out any foreign key references to the deleted DMO.
if (foreignNullers == null)
{
List<ForeignNuller> l = ForeignNuller.instances(this);
foreignNullers = Collections.unmodifiableList(l);
}
if (!foreignNullers.isEmpty())
{
ForeignNuller.breakLinkages(foreignNullers, currentRecord);
}
// before the actual deletion check all [object] fields and decrement the ref-counter
Set<String> ooFields = dmoInfo.getOoFields();
if (isTemporary && !ooFields.isEmpty())
{
Map<String, Method> pojoGetterMap = dmoInfo.getPojoGetterMap(); // caching it locally
for (String field : ooFields)
{
Method getter = pojoGetterMap.get(field);
PropertyMeta meta = dmoInfo.getPropsByGetterMap().get(getter);
int offset = meta.getOffset();
DataHandler handler = meta.getDataHandler();
//if (getter.getReturnType() == object.class)
{
Map<String, Integer> extents = dmoInfo.getExtentMap();
Integer extent = extents.get(field);
if (extent != null)
{
for (int k = 0; k < extent; k++)
{
object<?> someObject = (object<?>) handler.getField(currentRecord, offset + k);
decrementObjectCountRef(someObject);
}
}
else
{
decrementObjectCountRef((object<?>) handler.getField(currentRecord, offset));
}
}
}
}
// TODO: this probably belongs in the ORM layer (in Session?)
UniqueTracker.Token token = null;
try
{
// the whole table becomes invalid on delete
persistenceContext.getFastFindCache().invalidate(dmoInfo.getId(), getMultiplexID());
token = uniqueTrackerCtx.lockAndDelete(uniqueTracker, currentRecord);
persistence.delete(currentRecord);
}
finally
{
// if the deleted record was never flushed, remove it from the record nursery,
// so it is not flushed later
persistenceContext.getNursery().remove(this, currentRecord);
if (token != null)
{
uniqueTrackerCtx.unlock(uniqueTracker, token);
}
}
// notify dirty share context that this record is deleted
if (dirtyContext != null)
{
dirtyContext.delete(getEntityName(), currentRecord.primaryKey(), false, true);
}
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, describe() + ": record " + currentRecord.primaryKey() + " deleted");
if (LOG.isLoggable(Level.FINEST))
{
LOG.log(Level.FINEST, toString());
}
}
if (snapshot != currentRecord && undoable != null)
{
undoable.checkUndoable(true);
}
snapshot = currentRecord;
// notify listeners of the change, which will release the record in stateChanged() for this buffer
// and any other buffer listeners
reportChange(currentRecord, false, true);
Buffer buf = (Buffer) getDMOProxy();
if (buf.autoSynchronize().booleanValue())
{
buf.querySynchronize();
}
}
finally
{
deletedRecord.updateState(session, DmoState.DELETING, false);
}
// update the counter only if exception was not thrown
UserTableStatUpdater.delete(getDatabase(), this);
// everything went fine
return true;
}
/**
* Perform a bulk delete, restricted by the given where clause and query substitution
* parameters.
* <p>
* This method is invalid in base class so it will always throw an exception. It must be
* overwritten by {@link TemporaryBuffer#delete} with correct logic.
*
* @param suppDMOs
* The DMOs for the external (additional) buffers that are accessed in inner
* subselect, or {@code null} in case of a simple {@code where} predicate.
* @param where
* An FQL WHERE clause snippet which defines the restriction criteria to apply to the
* DELETE. All references to properties in a DMO must be unqualified.
* @param args
* Query substitution parameters required by the where clause.
*
* @throws PersistenceException
* always.
*/
protected void delete(DataModelObject[] suppDMOs, String where, Object... args)
throws PersistenceException
{
if (suppDMOs != null)
{
throw new IllegalArgumentException(
"This form of the method must be called only with temp-tables.");
}
delete(where, args);
}
/**
* Perform a bulk delete, restricted by the given where clause and query substitution
* parameters.
* <p>
* This default implementation uses a looping, record-by-record delete, which is substantially
* slower than an SQL-driven bulk delete. This is necessary for persistent tables which may be
* accessed concurrently.
*
* @param where
* An FQL where clause snippet which defines the restriction
* criteria to apply to the DELETE. All references to properties
* in a DMO must be unqualified.
* @param args
* Query substitution parameters required by the where clause.
*
* @throws PersistenceException
* if an error occurs performing the bulk delete operation.
*/
protected void delete(String where, Object... args)
throws PersistenceException
{
release(true);
PreselectQuery q = new PreselectQuery();
BlockManager.forEach(BlockManager.TransactionType.FULL, q, "loopAndDel",
new Block(
(Init) () ->
{
q.initialize(this, where, null, null, args);
q.setOmitAlias(true);
},
(Body) () ->
{
q.next();
try
{
delete();
}
catch (PersistenceException exc)
{
LOG.log(Level.SEVERE, "Error deleting record", exc);
}
}
)
);
}
/**
* Perform a bulk delete of all records in the table associated with this buffer.
* <p>
* DELETE FOR was also tested with Progress 4GL with WHERE clause. It is not needed for this
* method, but behaviour is the same. As result it is noticed that even such statement also
* waits for whole table even if only one record is locked independently whether this record
* satisfies WHERE clause or not . So, it seems idea of table-level locking is correct.
* <p>
*
* @throws IllegalArgumentException
* if no implementation class is found for the given database and interface;
* if setup for an invalid foreign relation fails.
* @throws StopConditionException
* if index information cannot be obtained for backing table or if database isn't
* connected.
*/
protected void deleteAll()
{
initialize();
// get EXCLUSIVE table lock, remembering current lock level
LockType previousTableLock;
try
{
previousTableLock = persistence.lockTable(table, LockType.EXCLUSIVE, true, !dmoInfo.multiTenant);
}
catch (LockUnavailableException e)
{
ErrorManager.recordOrThrowError(e);
return;
}
DataModelObject dmo = getDMOProxy();
try
{
P2JIndex primaryIndex = indexHelper.getPrimaryIndex(getEntityName());
String sort;
if (primaryIndex != null)
{
sort = primaryIndex.getOrderByClause(getDMOAlias(), P2JIndexComponent.PROPERTY);
}
else
{
sort = getDMOAlias() + "." + Session.PK + " asc";
}
final PreselectQuery query = new PreselectQuery().initialize(dmo, null, null, sort);
BlockManager.forEach(BlockManager.TransactionType.FULL, query, null, new Block()
{
public void body()
{
query.next();
try
{
delete();
}
catch (PersistenceException e)
{
ErrorManager.recordOrThrowError(e);
}
}
});
}
catch (PersistenceException e)
{
ErrorManager.recordOrThrowError(e);
}
catch (ConditionException | IllegalStateException | IllegalArgumentException exc)
{
ErrorManager.recordOrThrowError(new NumberedException(exc.getMessage(), exc));
}
finally
{
// set table lock back to previous level
try
{
persistence.lockTable(table, previousTableLock, true, !dmoInfo.multiTenant);
}
catch (LockUnavailableException e)
{
ErrorManager.recordOrThrowError(e);
}
}
}
/* *
* Perform a bulk delete of all records in the table associated with this
* buffer.
* <p>
* DELETE FOR was also tested with Progress 4GL with WHERE clause. It is not needed for this
* method, but behaviour is the same. As result it is noticed that even such statement also
* waits for whole table even if only one record is locked independently whether this record
* satisfies WHERE clause or not . So, it seems idea of table-level locking is correct.
* <p>
Below is Savepoint based approach for deleteAll.
Since we don't currently have simple answers for the Hibernate second-level cache or
for dealing with any records currently held in buffers, we'd like to first use a simple,
"converted-style" FOR-EACH loop as the basis for the RecordBuffer.deleteAll
implementation. This approach leverages the code we already have to deal with these
issues. If we find this causes memory problems, due to the bulk delete of very large
tables, we can revisit the savepoint implementation and figure out these remaining issues.
*/
/*
protected void deleteAll()
{
// TODO: current table lock level is queried/stored;
initialize();
try
{
flushAll();
}
catch (ValidationException exc)
{
reportValidationException(exc);
}
StringBuilder buf = new StringBuilder("delete from ");
String dmoImplName = getDMOImplementationName();
buf.append(dmoImplName);
String fql = buf.toString();
try {
int rowCount = persistence.deleteOrUpdate(fql, null, null);
if (rowCount > 0)
{
changeBroker.forcePendingFlush(this);
if (txHelper.isTransaction())
{
// force the records to be deleted, on db tx rollback
SavepointReversible rev = new SavepointReversible(getDMOName(),
"BULK DELETE",
persistence.bind());
bufferManager.addReversible(this, null, rev, false);
}
// Notify all interested parties that a bulk delete occurred.
// Listening buffers may need to release deleted records.
RecordChangeEvent.Type eventType = RecordChangeEvent.Type.FULL_BULK_DELETE;
changeBroker.stateChanged(eventType, this, null, null, null);
}
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
// TODO: exclusive table lock is acquired;
// TODO: original table lock level is restored.
}
*/
/**
* Reclaim the given list of primary keys for re-use.
*
* @param keys
* A list of primary keys which are available for re-use. Must
* not be <code>null</code>.
*/
protected void reclaimKeys(List<Long> keys)
{
/*
* 20160823 ECF: disabling primary key reclamation. This has proven to be unsafe in
* certain circumstances. Our record sorting algorithms rely on the natural order of
* primary key values to disambiguate the order of records which would otherwise
* sort equally (and thus ambiguously) using non-unique sort phrases. This can lead
* to deadlocks across sessions, due to inconsistent sorting. This only applies to
* permanent tables.
*/
/*
checkActive();
bufferManager.addPendingReclaimedKeys(persistence, keys);
*/
}
/**
* Get the cached {@link #dataModelAst} tree.
*
* @return See above.
*/
protected DataModelAst getDataModelAst()
{
return dataModelAst;
}
/**
* Cache the {@link #dataModelAst}.
*
* @param dataModelAst
* The data model AST.
*/
protected void setDataModelAst(DataModelAst dataModelAst)
{
this.dataModelAst = dataModelAst;
}
/**
* Get the cached {@link #schemaTableAst} tree.
*
* @return See above.
*/
protected ProgressAst getSchemaTableAst()
{
return schemaTableAst;
}
/**
* Cache the {@link #schemaTableAst}.
*
* @param schemaTableAst
* The schema table AST.
*/
protected void setSchemaTableAst(ProgressAst schemaTableAst)
{
this.schemaTableAst = schemaTableAst;
}
/**
* Get the number of fields in the legacy table represented by this buffer. Only normal fields
* are counted, the hidden fields are not taken into consideration.
*
* @return Number of fields.
*/
protected final int getNumFields()
{
if (numFields < 0)
{
numFields = lookupNumFields();
}
return numFields;
}
/**
* Look up the number of fields in the legacy table represented by this buffer.
*
* @return Number of fields.
*/
protected int lookupNumFields()
{
return TableMapper.getNumFields(getSchema(), dmoInfo.legacyTable);
}
/**
* Get bi-directional runtime mapping of DMO properties to their associated (lower case)
* legacy table names.
*
* @return Bi-directional runtime map of DMO properties to their associated (lower case)
* legacy table names.
*/
protected LegacyFieldNameMap getLegacyFieldNameMap()
{
if (legacyFieldNameMap == null)
{
legacyFieldNameMap = TableMapper.getLegacyFieldNameMap(this);
}
return legacyFieldNameMap;
}
/**
* Gets the legacy name assigned to a dynamically prepared temp-table. This implementation
* returns <code>null</code> and should be overridden by subclasses which require a
* non-<code>null</code> name to be returned.
*
* @return <code>null</code>.
*/
protected String getDynamicName()
{
return null;
}
/**
* Get the buffer manager associated with this buffer.
*/
protected BufferManager getBufferManager()
{
return bufferManager;
}
/**
* Increment the context local use count for the specific instance of the
* given record. Used in conjunction with {@link #decrementDMOUseCount} to
* track a DMO's in-use state.
* <p>
* Use counts must be maintained because multiple buffers may hold the
* same instance of a record. A record must not be evicted while any
* buffer still needs it.
*
* @param dmo
* DMO instance whose reference count is to be incremented.
* @param transient_
* {@code true} if this is a transient reference (usually from a presort query)
*
* @return Use count for <code>dmo</code> after incrementing.
*/
protected long incrementDMOUseCount(Record dmo, boolean transient_)
{
checkActive();
if (this.dirtyCopy)
{
return -1;
}
return dmo.incrementReferences(transient_);
}
/**
* Decrement the context local use count for the specific instance of the
* given record. Used in conjunction with {@link #incrementDMOUseCount} to
* track a DMO's in-use state.
* <p>
* Use counts must be maintained because multiple buffers may hold the
* same instance of a record. A record must not be evicted while any
* buffer still needs it.
*
* @param dmo
* DMO instance whose reference count is to be decremented.
* @param transient_
* {@code true} if this is a transient reference (usually from a presort query)
*
* @return Use count for <code>dmo</code> after decrementing.
*/
protected long decrementDMOUseCount(Record dmo, boolean transient_)
{
checkActive();
if (this.dirtyCopy)
{
return -1;
}
return dmo.decrementReferences(transient_);
}
/**
* Evict the given DMO from the current Hibernate session if it is not
* referenced by any buffer or other resource. This method does not
* modify the DMO's use count, if any. It is intended to quickly evict
* DMOs loaded into the session by side effect.
*
* @param dmo
* DMO instance whose reference count is to be decremented.
*
* @return <code>true</code> if the DMO was evicted; <code>false</code>
* if it was referenced by other resources.
*
* @throws PersistenceException
* if an error occurs detaching <code>dmo</code> from the
* persistence session.
*/
protected boolean evictDMOIfUnused(Record dmo)
throws PersistenceException
{
checkActive();
return bufferManager.evictDMOIfUnused(persistence, persistenceContext, dmo);
}
/**
* Report any pending changes in the current record's state to the {@link
* ChangeBroker} for broadcast to interested listeners. A change includes
* any modification to a property or the deletion of the record. If there
* is no change to report, this method returns immediately.
* <p>
* If this buffer has not previously taken a snapshot of its current
* record, a deep copy is made of <code>dmo</code> (which represents the
* post-modification state of this buffer's current record). Any
* unreported changes are then rolled back from this copy, so that the
* copy represents a snapshot of the record at the time it was first
* stored in the buffer.
* <p>
* This seemingly tortuous method of lazy initialization of the snapshot
* is used because it allows us in certain cases to avoid ever making a
* deep copy of the buffer's current record. If that record represents a
* Hibernate proxy which was never initialized, we avoid the overhead of
* ever hydrating the proxy, which in tight loops can result in a HUGE
* time savings.
* <p>
* The snapshot is then passed along with <code>dmo</code>, this buffer,
* and a map of property names to getter method indexes (for extent/array
* properties), to the change broker for dispatching to listeners.
*
* @param dmo
* DMO which represents the post-modification state of the
* current record. In the case of record deletion, this object
* represents the state of the record just before deletion.
* @param insert
* <code>true</code> if change represents a record insertion,
* otherwise <code>false</code>.
* @param delete
* <code>true</code> if change represents a record deletion,
* otherwise <code>false</code>.
*
* @throws PersistenceException
* if any reflection error occurs taking the snapshot, or if the
* change broker forwards an exception from one of its listeners.
*/
protected void reportChange(Record dmo, boolean insert, boolean delete)
throws PersistenceException
{
// sanity check
if (!delete && // is not a delete
!insert && // is not an insert
unreportedChanges == null) // is not an update
{
return;
}
try
{
Record snapshot = insert ? null : this.snapshot;
boolean takeSnapshot = (snapshot == null) && !insert;
if (takeSnapshot)
{
// DMO parameter represents the current record post-change (except
// in the delete case), so we deep copy it, then apply the old
// values selectively below, to reconstruct the original state.
// In the delete case, there are no diffs to apply, but we still
// need the snapshot.
snapshot = dmo.snapshot();
}
// Property/extent map for change event.
Map<String, List<Integer>> properties = null;
if (unreportedChanges != null)
{
try
{
PropertyMeta[] props = metadata.getPropertyMeta(false);
List<Integer> extentIndices = null;
int lastExtent = 0;
for (int k = 0; k < props.length; k++)
{
Object[] values = unreportedChanges[k];
if (values == null)
{
continue;
}
PropertyMeta prop = props[k];
int crtExtent = prop.getExtent();
if (crtExtent != 0) // is it an extent?
{
if (extentIndices == null || crtExtent != lastExtent)
{
extentIndices = new ArrayList<>();
if (properties == null)
{
properties = new HashMap<>();
}
properties.put(prop.getName(), extentIndices);
lastExtent = crtExtent;
}
extentIndices.add(k - prop.getOffset());
if (takeSnapshot)
{
OrmUtils.setField(snapshot, k, values[0]);
}
}
else // simple property
{
if (properties == null)
{
properties = new HashMap<>();
}
properties.put(prop.getName(), null);
if (takeSnapshot)
{
// apply old value of property to snapshot
OrmUtils.setField(snapshot, k, values[0]);
}
}
}
}
catch (Exception rflExc)
{
DBUtils.handleException(getDatabase(), rflExc);
throw new PersistenceException(rflExc);
}
}
if (properties == null)
{
properties = Collections.emptyMap();
}
if (LOG.isLoggable(Level.FINEST))
{
String desc = describe();
LOG.log(Level.FINEST, desc + ": DMO state changed...");
LOG.log(Level.FINEST, desc + ": Old state: " + toString(snapshot));
LOG.log(Level.FINEST, desc + ": New state: " + toString());
LOG.log(Level.FINEST, desc + ": Properties/Extents: " + properties);
}
// Notify listeners.
RecordChangeEvent.Type type = null;
if (insert)
{
type = RecordChangeEvent.Type.INSERT;
}
else if (delete)
{
type = RecordChangeEvent.Type.DELETE;
}
else
{
type = RecordChangeEvent.Type.UPDATE;
}
changeBroker.stateChanged(type, this, dmo, snapshot, properties);
}
finally
{
unreportedChanges = null;
}
}
/**
* Provide a short descriptor of this buffer instance for debugging and
* logging purposes. The descriptor includes the buffer's database name,
* short DMO interface name, its variable name, and the identifier for the
* current context.
*
* @return Short text descriptor.
*/
protected String describe()
{
StringBuilder buf = new StringBuilder("[");
buf.append(Utils.describeContext());
buf.append("-->");
if (isActive())
{
buf.append(getDatabase());
}
else
{
buf.append("'");
buf.append(ldbOrAlias);
buf.append("'");
}
buf.append(".");
buf.append(getDMOName());
Integer mpid = getMultiplexID();
if (mpid != null)
{
buf.append(":");
buf.append(mpid);
}
buf.append("@");
buf.append(bufferManager.getOpenBufferScopes());
buf.append(" (");
buf.append(variable);
buf.append(") ");
buf.append(txHelper.currentTransactionLevel());
buf.append("]");
return buf.toString();
}
/**
* Attempt to upgrade to an exclusive lock on the current record, if we do
* not already have one. The current context must already possess a share
* lock (or greater) for this method to complete successfully. The current
* thread will block inside the lock manager if the lock is not immediately
* available. When this method returns normally, the caller can assume the
* lock has been acquired.
*
* @throws PersistenceException
* if the current context does not have a share lock (or greater)
* on the current record.
*/
public void upgradeLock()
throws PersistenceException
{
if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && currentRecord.primaryKey() == null)
{
return; // the records created by the default tenant are volatile. No locking in this case.
}
RecordIdentifier<String> ident = currentRecord.getRecordLockIdentifier();
LockType currentType = persistence.queryLock(ident, !dmoInfo.multiTenant);
if (currentType == LockType.NONE)
{
throw new PersistenceException(
getLegacyName() + " record has NO-LOCK status, update to field not allowed", 396);
}
persistenceContext.getRecordLockContext().lock(ident, LockType.EXCLUSIVE, 0L, this);
}
/**
* Load the {@code DataModelObject} sent as parameter in this buffer with associate it with the persistence
* session of the buffer. It will automatically receive a primary key and all properties will be marked as
* 'dirty' to allow the object to be saved to database. If there is a record already in the buffer, that
* record is validated, its WRITE trigger fired and then flushed to database before setting the new record.
*
* @param dmo
* The object to be associated.
*
* @throws PersistenceException
* if there was an error while retrieving the next primary key.
*/
public void associate(DataModelObject dmo)
throws PersistenceException
{
if (!(dmo instanceof Record) || ((Record) dmo).primaryKey() != null)
{
throw new IllegalArgumentException("Invalid DMO: not a Record or DMO is already associated.");
}
Record asRecord = (Record) dmo;
BitSet dirtyProps = asRecord.getDirtyProps(); // this assumes the original field is returned, not a copy
RecordMeta rm = asRecord._getRecordMeta();
boolean sharedDb = !rm.getDmoMeta().multiTenant;
dirtyProps.set(0, rm.getPropertyMeta(false).length);
asRecord.updateState(persistence.getSession(sharedDb), DmoState.NEW | DmoState.CHANGED, true);
asRecord.primaryKey(nextPrimaryKey());
setCurrentRecord(asRecord, false, true, false, true);
}
/**
* Retrieve the set of all properties for this buffer's DMO entity, which represent database columns that
* participate in an index on the entity's backing table, or only the subset which participate in unique
* indexes.
*
* @param unique
* {@code false} to get all indexed properties; {@code true} to get only those that participate
* in unique indexes.
*
* @return BitSet of indexed properties. The set may be empty but it will never be {@code null}.
*/
private BitSet getIndexedProperties(boolean unique)
{
if (unique)
{
return metadata.getAllIndexedProperties();
}
BitSet[] indices = metadata.getUniqueIndices();
BitSet ret = new BitSet();
for (BitSet idx : indices)
{
ret.or(idx);
}
return ret;
}
/**
* Generate a formatted description of the contents of {@code record}.
* The report includes the name and current value of every data property within the record.
*
* @param record
* Record whose contents are to be written to the report.
*
* @return A formatted report of {@code record}'s contents.
*/
protected String toString(Record record)
{
boolean active = isActive();
String sep = System.getProperty("line.separator");
StringBuilder buf = new StringBuilder();
buf.append(active ? getDatabase() : ldbOrAlias);
buf.append(" ");
buf.append(getDMOName());
buf.append(" (");
buf.append(getDMOAlias());
buf.append("@");
buf.append(bufferManager.getOpenBufferScopes());
if (!active)
{
buf.append(") - INACTIVE");
return buf.toString();
}
buf.append(":");
buf.append(activeScopeDepth);
buf.append("#");
buf.append(openScopeCount);
buf.append(") [");
buf.append(getTable());
buf.append("]").append(sep);
buf.append("---------------------------------------------").append(sep);
if (record == null)
{
buf.append(" No record");
return buf.toString();
}
buf.append(record.toString(false)).append(sep);
buf.append("---------------------------------------------").append(sep);
if (metadata != null)
{
String name = null;
PropertyMeta[] propMeta = metadata.getPropertyMeta(false);
int len = propMeta.length;
String[] names = new String[len];
int maxWidth = Session.PK.length();
for (int i = 0; i < len; i++)
{
PropertyMeta next = propMeta[i];
name = next.getLegacyName();
int extent = next.getExtent();
if (extent > 0)
{
name = name + '[' + extent + ']';
}
names[i] = name;
maxWidth = Math.max(maxWidth, name.length());
}
buf.append(" ");
StringHelper.leftAlignText(Session.PK, maxWidth, buf);
buf.append(" : ");
buf.append(record.primaryKey());
Object[] data = record.getData(this);
for (int i = 0; i < len; i++)
{
name = names[i];
buf.append(sep);
buf.append(" ");
StringHelper.leftAlignText(name, maxWidth, buf);
buf.append(" : ");
buf.append(formatProperty(data[i]));
}
}
buf.append(sep);
return buf.toString();
}
/**
* Returns the primary key for a new DMO associated with this buffer.
*
* @return The primary key for a new DMO associated with this buffer.
*
* @throws PersistenceException
* if there was an error while retrieving the next primary key.
*/
protected Long nextPrimaryKey()
throws PersistenceException
{
checkActive();
return persistence.nextPrimaryKey(table, !dmoInfo.multiTenant);
}
/**
* Initializes the map that contains primary keys of past and present records held by this buffer,
* mapped to their pinned LockTypes.
* <p>
* Initialize the set which will hold the primary keys of records affected by a rollback in the current
* scope.
*/
protected void initPinnedLockTypes()
{
pinnedLocks = new PinnedLocks();
}
/**
* Decrements the counter-reference for the specified object.
* <p>
* This is only active for TEMP-TABLES, for permanent tables this has no effect as they cannot
* store {@code object} fields.
*
* @param obj
* The object whose reference need to be decremented.
*/
protected void decrementObjectCountRef(object<?> obj)
{
}
/**
* Check if this buffer is associated with a global buffer.
*
* @return Always <code>false</code>.
*/
protected boolean isGlobal()
{
return false;
}
/**
* Get the persistence context for this buffer.
*
* @return Persistence context, which will be <code>null</code> if this buffer has not yet
* been initialized.
*/
protected Persistence.Context getPersistenceContext()
{
return persistenceContext;
}
/**
* Register a query that should be closed when the scope of this buffer is closed.
*
* @param query
* Query to register.
*/
protected void registerRelatedQuery(P2JQuery query)
{
if (relatedQueries == null)
{
relatedQueries = new HashSet<>();
}
relatedQueries.add(query);
}
/**
* Unregister a query from the list of the queries that should be closed when the scope of
* this buffer is closed.
*
* @param query
* Query to unregister.
*/
protected void unregisterRelatedQuery(P2JQuery query)
{
if (relatedQueries != null)
{
relatedQueries.remove(query);
}
}
/**
* Get the table/buffer legacy name. If the legacy name was temporarily overridden, then that name is
* returned instead until the name is restored back to old name.
*
* @return The legacy name explicitly set by business logic, or if none, the default legacy table name.
*/
protected String getLegacyName()
{
if (overrideLegacyName != null)
{
return overrideLegacyName;
}
String name = dmoProxy != null ? ((BufferImpl) dmoProxy)._name() : null;
return name == null ? dmoInfo.legacyTable : name;
}
/**
* Aggregate method which averages the values of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* FQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
protected void average(String column, NumberType num, String where, Object... args)
{
aggregate("avg", column, num, where, args);
}
/**
* Aggregate method which counts the rows in the table backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate. May be the wildcard
* character ("*").
* @param num
* Variable into which to store the aggregate result.
* @param where
* FQL where clause used to filter the records.
* @param args
* Optional query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
protected void count(String column, NumberType num, String where, Object... args)
{
aggregate("count", column, num, where, args);
}
/**
* Aggregate method which finds the minimum value of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* FQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
protected void minimum(String column, NumberType num, String where, Object... args)
{
aggregate("min", column, num, where, args);
}
/**
* Aggregate method which finds the maximum value of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* FQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
protected void maximum(String column, NumberType num, String where, Object... args)
{
aggregate("max", column, num, where, args);
}
/**
* Aggregate method which sums the values of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* FQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
protected void sum(String column, NumberType num, String where, Object... args)
{
aggregate("sum", column, num, where, args);
}
/**
* Indicate whether this buffer is active. A buffer is active if it has
* been used by a query or otherwise initialized. Opening a buffer scope
* alone is not enough to activate a buffer, but it is a prerequisite to
* activation; the buffer must actually be used to be considered active.
*
* @return <code>true</code> if the buffer has been initialized.
*/
protected boolean isActive()
{
return (persistence != null);
}
/**
* Record or throw a "record not on file" error condition for
* the current DMO type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
protected void errorNotOnFile()
{
final String msg = getLegacyName() + " record not on file";
if (LOG.isLoggable(Level.FINE))
{
PersistenceException exc = new PersistenceException(msg, 138);
LOG.log(Level.FINE, msg, exc);
}
ErrorManager.recordOrThrowError(138, msg);
}
/**
* Get the number of scopes currently open for this buffer. This is
* necessary because buffer scopes can be nested.
*
* @return Number of scopes currently open for this buffer.
*/
protected int getOpenScopeCount()
{
return openScopeCount;
}
/**
* Validate and release the current record in this buffer and release its
* lock, if any, according to the following rules:
* <ul>
* <li>the lock is released at the later of:
* <ul>
* <li>end of the current transaction (if any);
* <li>end of the current buffer scope.
* </ul>
* <li>the lock is downgraded at the end of the current transaction,
* if the current buffer scope is larger than the transaction scope.
* <li>if the lock never passed through a transaction, it is released
* immediately.
* </ul>
*
* @param allowWriteTrigger
* {@code true} to allow a write trigger to be fired on this release. This generally
* should be {@code true} for non-iterating queries and releases invoked explicitly
* by business logic. It should be {@code false} for iterating queries. Other implicit
* releases depend on the situation.
*
* @throws ErrorConditionException
* if validation of the current buffer record (if any) fails.
*/
protected void release(boolean allowWriteTrigger)
{
release(false, allowWriteTrigger);
}
/**
* Possibly validate and release the current record in this buffer and release its
* lock, if any, according to the following rules:
* <ul>
* <li>the lock is released at the later of:
* <ul>
* <li>end of the current transaction (if any);
* <li>end of the current buffer scope.
* </ul>
* <li>the lock is downgraded at the end of the current transaction,
* if the current buffer scope is larger than the transaction scope.
* <li>if the lock never passed through a transaction, it is released
* immediately.
* </ul>
*
* @param undo
* {@code true} if currently processing an undo, else {@code false}. The outgoing
* record, if any, is not validated and flushed if this parameter is {@code true}.
* @param allowWriteTrigger
* {@code true} to allow a write trigger to be fired on this release. This generally
* should be {@code true} for non-iterating queries and releases invoked explicitly
* by business logic. It should be {@code false} for iterating queries. Other implicit
* releases depend on the situation.
*
* @throws ErrorConditionException
* if validation of the current buffer record (if any) fails.
*/
protected void release(boolean undo, boolean allowWriteTrigger)
{
release(undo, allowWriteTrigger, true);
}
/**
* Possibly validate and release the current record in this buffer and release its
* lock, if any, according to the following rules:
* <ul>
* <li>the lock is released at the later of:
* <ul>
* <li>end of the current transaction (if any);
* <li>end of the current buffer scope.
* </ul>
* <li>the lock is downgraded at the end of the current transaction,
* if the current buffer scope is larger than the transaction scope.
* <li>if the lock never passed through a transaction, it is released
* immediately.
* </ul>
*
* @param undo
* {@code true} if currently processing an undo, else {@code false}. The outgoing
* record, if any, is not validated and flushed if this parameter is {@code true}.
* @param allowWriteTrigger
* {@code true} to allow a write trigger to be fired on this release. This generally
* should be {@code true} for non-iterating queries and releases invoked explicitly
* by business logic. It should be {@code false} for iterating queries. Other implicit
* releases depend on the situation.
* @param honorMultipleReferences
* {@code true} if the released record should be flushed only if this buffer was the
* only one referencing it. If this is {@code false}, even if the release buffer is
* loaded in other buffers, it will be flushed.
*
* @throws ErrorConditionException
* if validation of the current buffer record (if any) fails.
*/
protected void release(boolean undo, boolean allowWriteTrigger, boolean honorMultipleReferences)
{
if (currentRecord != null)
{
if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && currentRecord.primaryKey() == null)
{
ErrorManager.throwError(15991, getLegacyName(), ""); // non-silentiable
// Attempt to create in multi-tenant table '<tablename>' where the partition (usually default) has not been allocated. (15991)
return;
}
setCurrentRecord(null, undo, false, false, allowWriteTrigger, honorMultipleReferences);
}
Buffer bufferImpl = (Buffer) dmoProxy;
if (bufferImpl.autoSynchronize().booleanValue())
{
bufferImpl.querySynchronize();
}
// be sure the flag is reset even if the buffer remains empty
if (!undo)
{
unknownMode = false;
}
}
/**
* Release the current record and discard the reference record snapshot.
*/
protected void reset()
{
this.reversibleOffend = false;
// do not allow WRITE trigger to fire; this is only called in the context of an iterating query
release(false);
if (snapshot != null && undoable != null)
{
undoable.checkUndoable(true);
}
snapshot = null;
}
/**
* Discard the reference record snapshot.
*/
protected void resetSnapshot()
{
snapshot = null;
}
/**
* Reload the current record, if any, from the database. If {@code lockType} is higher than
* the current lock type, it is upgraded immediately. If lower, it is downgraded immediately
* only if outside of a transaction. If inside a transaction, the lock is marked for downgrade
* to the specified type upon transaction end.
*
* @param lockType
* Requested lock type.
* @param errorIfNull
* {@code true} if the current record being null should raise an error; else
* {@code false}.
*
* @throws ErrorConditionException
* if there is no record loaded into the buffer, the error manager is not in silent
* mode, and the caller considers this an error.
*/
protected void reload(LockType lockType, boolean errorIfNull)
{
if (currentRecord == null && !errorIfNull)
{
// we have no current record from which to get a primary key to perform a reload, and
// this is not considered an error by the caller, so nothing to do
return;
}
load(currentRecord, lockType, false, true);
}
/**
* Loads the template record for this buffer. The template cannot be altered (at least without
* acquiring the database schema lock) and cannot be saved back to database. It must not reach
* the ORM session.
* <p>
* By convention, the template records have negative rowid/recid.
*
* @param templateId
* The id of the template record.
*
* @throws IllegalArgumentException
* if the {@code templateId} is not the rowid of a template record (it is positive).
*/
protected void loadTemplateRecord(long templateId)
{
Long actualTemplateId = MetadataManager.getTemplateRowid(getDMOImplementationClass());
if (actualTemplateId == null || actualTemplateId != templateId)
{
// P2J metadata is not active or the template is incorrect
throw new IllegalArgumentException("Error loading template record");
}
Record dmo = null;
try
{
dmo = instantiateDMO();
}
catch (ReflectiveOperationException exc)
{
throw new RuntimeException("Error creating record", exc);
}
dmo.primaryKey(templateId);
setCurrentRecord(dmo, false, false, false, false);
sortIndex = null;
offEnd = OffEnd.NONE;
}
/**
* Set the DMO implementation instance which backs this buffer. It is expected that this
* variant is invoked only by queries storing their retrieved record.
* <p>
* Setting the value to {@code null} may result in an error or query off-end condition,
* depending upon the value of {@code errorIfNull} and the current silent error mode.
* <p>
* It is expected that the calling query specify the sort index which was in use and whether
* the query ran off-end (and in which direction). This information is useful to {@link
* FindQuery find queries} which must determine an appropriate starting point for their search.
* <p>
* Note: the access level of this method must be at least {@code protected}. Although this is not required
* at compile time, some applications may stop with a {@code NullPointerException} because the
* not be proxied otherwise.
*
* @param currentRecord
* DMO instance which represents the record currently backing this buffer.
* @param lockType
* Lock type (already applied to a non-{@code null} {@code currentRecord}), which will
* be pinned by this record buffer.
* @param errorIfNull
* {@code false} to raise an end condition if the record being set is {@code null};
* else {@code true} (caller is responsible for handling appropriately in the latter case).
* @param lenientOffEnd
* {@code false} to raise an end condition if the record being set is {@code null};
* else {@code true} (caller is responsible for handling appropriately in the latter case).
* @param sortIndex
* Sort index which was in use at the time the given record was fetched.
* @param offEnd
* Enum indicating whether query is off-end, and if so, in which direction.
* @param inverseSorting
* Indicates whether {@code offEnd} is specified against the inverse sorting direction.
* @param dirtyCopy
* Indicates whether {@code currentRecord} represents a copy of a DMO found in the
* dirty database.
* @param allowWriteTrigger
* {@code true} to allow a WRITE trigger to be fired on this event, else {@code false}.
*
* @throws QueryOffEndException
* if {@code currentRecord} is {@code null} and {@code errorIfNull} is {@code false}.
*
* @see #getOffEnd(SortIndex,boolean)
*/
protected void setRecord(Record currentRecord,
LockType lockType,
boolean errorIfNull,
boolean lenientOffEnd,
SortIndex sortIndex,
OffEnd offEnd,
boolean inverseSorting,
boolean dirtyCopy,
boolean allowWriteTrigger)
throws QueryOffEndException
{
setCurrentRecord(currentRecord, false, false, dirtyCopy, allowWriteTrigger);
this.sortIndex = sortIndex;
this.offEnd = inverseSorting ? offEnd.inverse() : offEnd;
if (currentRecord == null)
{
if (!errorIfNull && !lenientOffEnd)
{
throwOffEnd();
}
}
else
{
Long recId = currentRecord.primaryKey();
if (lockType != LockType.NONE)
{
RecordIdentifier<String> ident = currentRecord.getRecordLockIdentifier();
persistenceContext.getRecordLockContext().registerBuffer(ident, this);
}
setPinnedLockType(recId, lockType);
DatabaseEventType det = DatabaseEventType.FIND;
if (triggerTracker != null && triggerTracker.isTriggerEnabled(det, null, logicalDatabase))
{
if (triggerTracker.isExecuting(det, recId))
{
DatabaseTriggerManager.handleError(2873, det, dmoInfo.legacyTable, null);
}
else
{
// when everything is done, execute find triggers
ErrorManager.ErrorHelper err = ErrorManager.getErrorHelper();
boolean wasSilent = err.isSilent();
if (!wasSilent)
{
err.setSilent(true);
}
try
{
triggerTracker.pushEvent(det, recId);
if (!DatabaseTriggerManager.trigger(det,
dmoBufInterface,
(Buffer) dmoProxy,
null,
logicalDatabase))
{
setCurrentRecord(null, false, false, false, false);
}
}
finally
{
triggerTracker.popEvent(recId);
if (!wasSilent)
{
err.setSilent(false);
}
}
}
}
UserTableStatUpdater.retrieve(getDatabase(), this);
}
}
/**
* Throw a <code>QueryOffEndException</code>.
*
* @throws QueryOffEndException
* always.
*/
protected void throwOffEnd()
{
throw new QueryOffEndException("", new QueryOffEndListener[] { offEndListener });
}
/**
* Get an indication whether the last current record set by a query:
* <ol>
* <li>was <code>null</code> (meaning the query was off-end), and if so
* <li>whether it used the same sort index as the one specified, and if
* so
* <li>in which direction the query ran off-end.
* </ol>
* <p>
* In the event no query has yet set a record in this buffer, a value of
* <code>OffEnd.FRONT</code> will be returned. In the event the last query
* neither ran off-end, nor used the given sort index, a value of
* <code>OffEnd.NONE</code> will be returned.
*
* @param sortIndex
* Sort index to compare with the last sort index used to
* populate (or clear) the buffer.
* @param inverseSorting
* Indicates whether the off-end is requested by a query having
* the inverse sorting direction.
*
* @return See above.
*
* @see #setRecord
*/
protected OffEnd getOffEnd(SortIndex sortIndex, boolean inverseSorting)
{
if (this.sortIndex == null || this.sortIndex.equals(sortIndex))
{
return inverseSorting ? offEnd.inverse() : offEnd;
}
return OffEnd.NONE;
}
/**
* Set the off-end status for the query most recently involving this buffer.
*
* @param offEnd
* Off-end status.
*/
protected void setOffEnd(OffEnd offEnd)
{
this.offEnd = offEnd;
}
/**
* Get the {@link QueryOffEndListener} instance for this record buffer.
*
* @return the {@link QueryOffEndListener} instance for this record buffer.
*/
protected QueryOffEndListener getQueryOffEndListener()
{
return offEndListener;
}
/**
* Push a temporary record context onto the buffer for purposes such as
* client-side sorting. Such a context must be pushed before {@link
* #setTempRecord(Record) setting a temporary record} is possible.
* A context allows one or more temporary records to be set for a specific
* operation, such as the execution of client-side where clause
* expressions by a query, and for such operations to be nested. For
* example, a client-side where expression may eventually execute a query
* which then itself may execute a client-side where expression. Each
* level of client-side where clause expression must execute within its
* own temporary record context, such that the state of the outer where
* clause expression is not lost when the inner expression is finished.
* When client code is finished with a temporary record context, it should
* {@link #popTempContext() pop} the current context.
* <p>
* Pushes and pops of the temporary record context must be balanced. This
* is the responsibility of the caller.
*/
protected void pushTempContext()
{
if (tempRecords == null)
{
// It is rare that we would need a depth greater than 1, so make the
// default size 1.
tempRecords = new ArrayList<>(1);
}
tempRecords.add(null);
}
/**
* Pop the current, temporary record context from this buffer.
* <p>
* As a convenience, the currently active temporary record for this buffer
* is set to <code>null</code> first.
*
* @throws PersistenceException
* if setting the current, temporary record to <code>null</code>
* triggers a failure to evict the temporary record from the
* active Hibernate session.
*
* @see #pushTempContext()
*/
protected void popTempContext()
throws PersistenceException
{
setTempRecord(null);
tempRecords.remove(tempRecords.size() - 1);
}
/**
* Set a temporary record into the buffer for purposes such as client-side
* sorting. Setting a temporary record has no impact on locking or
* validation. It is essentially a read-only record which temporarily
* backs the buffer proxy, so that non-updating client code can run using
* the buffer.
* <p>
* The default value of the temporary record is <code>null</code>. It is
* only set to a non-<code>null</code> reference in order to do some work
* with the buffer. When the work is complete, it is critical that the
* temporary record be set back to <code>null</code>.
* <p>
* Temporary records increment the context-wide DMO use count while they
* are active and decrement it when they are released. This may result
* in a temporary record being evicted from the persistence session when
* it is replaced.
*
* @param tempRecord
* Record which will temporarily back the buffer.
*/
protected void setTempRecord(Record tempRecord)
{
Record old = getTempRecord();
if (old != null)
{
decrementDMOUseCount(old, true);
}
tempRecords.set(tempRecords.size() - 1, tempRecord);
if (tempRecord != null)
{
incrementDMOUseCount(tempRecord, true);
}
}
/**
* Get the combined DMO and Buffer grouping interface associated with this buffer.
*
* @return DMO and Buffer grouping interface.
*/
protected Class<? extends Buffer> getDMOBufInterface()
{
return dmoBufInterface;
}
/**
* Report the DMO implementation class associated with this buffer.
*
* @return DMO implementation class.
*
* @throws IllegalStateException
* if the buffer fails the initialization verification.
*/
protected Class<? extends Record> getDMOImplementationClass()
{
initialize();
return dmoClass;
}
/**
* Get the unqualified name of the DMO's interface.
*
* @return DMO interface short name.
*/
protected String getDMOName()
{
return Utils.getUnqualifiedName(dmoInterface);
}
/**
* Get the unqualified name of the DMO implementation class.
*
* @return DMO short class name.
*/
protected String getDMOImplementationName()
{
return Utils.getUnqualifiedName(getDMOImplementationClass());
}
/**
* Get the variable name associated with this buffer instance.
*
* @return Variable name associated with the buffer.
*/
public String getDMOAlias()
{
return variable;
}
/**
* Get the token associated with this buffer instance.
*
* @return Token name associated with the buffer.
*/
public String getDMOToken()
{
return token;
}
/**
* Report whether this buffer is in unknown mode, due to an outer join not finding a record.
*
* @return <code>true</code> if in unknown mode, else <code>false</code>.
*/
public boolean isUnknownMode()
{
return unknownMode;
}
/**
* Makes a backup of the current variable and it replaces it with another one.
* This is usually used by an argument proxy to temporarily replace the DMO alias.
* The override is successful only if the DMO alias wasn't overridden beforehand.
*
* @param newVariable
* The new DMO alias which should be temporarily used by this RecordBuffer.
*
* @return {@code true} if the overriding was successful, {@code false} otherwise.
*/
protected boolean overrideDMOAlias(String newVariable)
{
if (oldVariable != null)
{
return false;
}
oldVariable = variable;
variable = newVariable;
return true;
}
/**
* Restores the DMO alias by replacing the current variable with the one stored in the backup.
*/
protected void restoreDMOAlias()
{
variable = oldVariable;
oldVariable = null;
}
/**
* Makes a backup of the current legacy name and temporarily it replaces it with another one. This is used
* to avoid buffer name collisions when working with dynamic buffers/queries.
* The override is successful only if the DMO name wasn't overridden beforehand. Be sure to call the
* {@code restoreLegacyName} to restore the real name.
*
* @param newName
* The new legacy name which should be temporarily used by this RecordBuffer.
*
* @return {@code true} if the overriding was successful, {@code false} otherwise.
*/
protected boolean overrideLegacyName(String newName)
{
if (overrideLegacyName != null)
{
return false;
}
overrideLegacyName = newName;
return true;
}
/**
* Restores the DMO alias by replacing the current variable with the one stored in the backup.
*/
protected void restoreLegacyName()
{
overrideLegacyName = null;
}
/**
* Get the persistence service object associated with this buffer. This
* object is the buffer's interface to the physical database which backs
* it.
*
* @return Persistence service object for this buffer's database.
*/
public Persistence getPersistence()
{
checkActive();
return persistence;
}
/**
* Get the physical database name associated with this buffer.
*
* @return Physical database name.
*/
public Database getDatabase()
{
if (persistence != null)
{
return persistence.getDatabase(!dmoInfo.multiTenant);
}
if (logicalDatabase == null)
{
// retrieve logical database name
logicalDatabase = getLDBName(ldbOrAlias);
// database hasn't been connected
if (logicalDatabase == null)
{
int[] num = new int[] {855, 725};
String[] text = new String[2];
text[0] = "Unknown database name " + ldbOrAlias;
text[1] = "Unknown or ambiguous table " + getDMOName();
ErrorManager.showErrorAndAbend(num, text);
return null;
}
}
// look up physical database associated with logical name
Database database = getPDB(logicalDatabase);
if (MetadataManager.isMetaDMO(dmoInterface))
{
// a metadata buffer will be querying its data from an embedded, metadata database
database = database.toType(Database.Type.META);
}
return database;
}
/**
* Get the schema name of the database associated with this buffer.
*
* @return Database schema name.
*/
protected String getSchema()
{
return DatabaseManager.getSchema(getDatabase());
}
/**
* Get the logical database name associated with this buffer.
*
* @return Logical database name.
*/
protected String getLogicalDatabase()
{
initialize();
return logicalDatabase;
}
/**
* Get the name of the table associated with this buffer.
*
* @return Name of SQL table backing this record buffer.
*/
protected String getTable()
{
if (table == null)
{
table = dmoInfo.getSqlTableName();
}
return table;
}
/**
* Get the dirty share context used by this buffer.
*
* @return Dirty share context for this buffer's database.
*/
protected DirtyShareContext getDirtyContext()
{
checkActive();
return dirtyContext;
}
/**
* Get the record lock context helper.
*
* @return Record lock context helper.
*/
protected RecordLockContext getRecordLockContext()
{
checkActive();
return persistenceContext.getRecordLockContext();
}
/**
* Enter into a safe mode where current record is <code>null</code>, but
* calls to DMO getter methods return unknown value instead of raising an
* error condition. This is intended to support outer join.
*/
protected void setUnknownMode()
{
unknownMode = true;
}
/**
* Instantiate a data record to back the specified DMO and to be added to the database upon transaction
* commit. The record's data members are initialized to their default values. The created record becomes
* the buffer's current record. A share lock is automatically applied to the new record. Foreign keys, if
* any, are synchronized using the default values of legacy key properties.
*
* @return {@code true} if operation is successful and a fresh new record can be found in the buffer.
*
* @throws ErrorConditionException
* if there is an error retrieving the next available primary key
* from the database or locking the record.
*/
protected boolean create()
{
if (vst || readonly)
{
// TODO: find the proper message to throw when attempting to create a record in a readonly table.
// this is quite fatal, NO-ERROR will not prevent it from stopping the procedure
ErrorManager.throwError(6248,
"Creating records in a Virtual System Table is not allowed",
null,
false);
return false;
}
// check the before buffer constraints
BufferImpl buf = (BufferImpl) dmoProxy;
if (isTemporary && buf.isBeforeBuffer() && buf.hasBeforeBufferConstraints())
{
ErrorManager.recordOrThrowError(12373, buf.doGetName(), "");
// You may not create BEFORE-TABLE <name> record.
return false;
}
initialize();
bufferManager.registerDirtyBuffer(this);
Long recId = null;
RecordIdentifier<String> ident = null;
try
{
if (isTransient())
{
if (currentRecord.checkState(DmoState.INVALID))
{
// release the broken record as if undone
// TODO: is this still correct, now that we no longer update the DMO state to INVALID on a
// failed, explicit validation?
setCurrentRecord(null, true, false, false, false);
}
else
{
// flush the transient record currently in the buffer, which will attempt validation;
// the CREATE method will be aborted if this record is invalid
flush(true);
}
}
// create new record with default properties and starting state
Record record = instantiateDMO();
record.initialize(getSession(), true);
// in case of multi-tenant without default tables, transient records CAN BE CREATED without PK,
// but they are NEVER flushed
boolean noDefaultTenant = !dmoInfo.multiTenant ||
dmoInfo.multiTenantWithDefault ||
persistenceContext.getTenantId() != TenantManager.DEFAULT_TENANT_ID;
if (noDefaultTenant)
{
// generate and assign a unique key (the fact that we are doing this test here means that probably
// in 4GL this does not happen at this moment, but rather at first access of PK)
recId = nextPrimaryKey();
record.primaryKey(recId);
// acquire a default share lock on the new record
ident = record.getRecordLockIdentifier();
RecordLockContext rlc = persistenceContext.getRecordLockContext();
rlc.lock(ident, LockType.SHARE, 0L, this);
rlc.registerBuffer(ident, this);
}
// set the new record into the buffer
setCurrentRecord(record, false, true, false, true);
if (noDefaultTenant)
{
// store the record temporarily in the record nursery, until it gets flushed to the database;
// it is stored only under the "special" recid index for now
persistenceContext.getNursery().indexUpdated(this, null, true, record);
}
// remember that this record has not been persisted yet; an eventual trigger must know
// the record is still transient.
createScope = bufferManager.getOpenBufferScopes();
// invoke callback procedures first. Any trigger procedure is executed after this event is handled
if (isTemporary)
{
if (buf._dataSet() != null)
{
// checks whether tracking is active
if (getParentTable()._isTrackingChanges())
{
try
{
createRowCallback = true;
if (!buf.invokeCallback(Buffer.ROW_CREATE_EVENT))
{
BlockManager.returnError();
}
}
finally
{
createRowCallback = false;
}
if (currentRecord == null)
{
// the callback procedure/method deleted the newly created transient record
// nothing more to do, just return
return false;
}
}
}
}
DatabaseEventType det = DatabaseEventType.CREATE;
if (triggerTracker != null)
{
if (triggerTracker.isTriggerEnabled(det, null, logicalDatabase))
{
if (triggerTracker.isExecuting(det, recId))
{
DatabaseTriggerManager.handleError(3168, det, dmoInfo.legacyTable, null);
}
else
{
// when everything is done, execute create triggers
try
{
triggerTracker.pushEvent(det, recId);
if (!DatabaseTriggerManager.trigger(det,
dmoBufInterface,
(Buffer) dmoProxy,
null,
logicalDatabase))
{
// this will happen only in no-error mode if the trigger vetoed the create operation
setCurrentRecord(null, false, false, false, false);
return false;
}
}
finally
{
triggerTracker.popEvent(recId);
}
}
}
}
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, describe() + ": new record created");
if (LOG.isLoggable(Level.FINEST))
{
LOG.log(Level.FINEST, toString());
}
}
// the whole table becomes invalid on creation of new records, even if not yet committed
persistenceContext.getFastFindCache().invalidate(dmoInfo.getId(), getMultiplexID());
// save the current record into the snapshot
if (undoable != null)
{
undoable.checkUndoable(true);
}
snapshot = currentRecord.snapshot();
armWriteTrigger(this, false); // sets new record's snapshot as the "old" DMO for write trigger
// null out [ident] to indicate we completed successfully
ident = null;
// if this is buffer of an active AFTER-BUFFER, update the BEFORE-TABLE, too:
BufferImpl after = (BufferImpl) dmoProxy;
if (!ignoreBeforeTracking &&
after.isAfterBuffer() &&
((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) after))._isTrackingChanges())
{
BufferImpl before = after.beforeBufferNative();
if (before != null)
{
// create new before record image, then update my before-rowid and row-state
before.setUpBeforeBuffer(true);
before.create();
RecordBuffer beforeBuf = before.buffer();
RecordBuffer afterBuf = after.buffer();
beforeBuf.rowState(Buffer.ROW_CREATED);
afterBuf.rowState(Buffer.ROW_CREATED);
beforeBuf.peerRowid(after.rowID());
afterBuf.peerRowid(before.rowID());
before.setUpBeforeBuffer(false);
// do not release the before buffer, let it hold the newly created row
}
// else there's already a record in before-table related to this modified. Do not add!
}
if (buf.autoSynchronize().booleanValue())
{
buf.querySynchronize();
}
}
catch (ValidationException exc)
{
reportValidationException(exc, this, false);
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
catch (IllegalAccessException | InstantiationException exc)
{
throw new RuntimeException("Error creating record", exc);
}
catch (InvocationTargetException exc)
{
DBUtils.handleException(getDatabase(), exc);
throw new RuntimeException("Error creating record", exc);
}
catch (ReflectiveOperationException exc)
{
throw new RuntimeException("Error creating record", exc);
}
catch (Throwable t)
{
// for debug only:
LOG.info("Rethrowing:", t);
// then rethrow:
throw t;
}
finally
{
// if ident is not null, we are not exiting normally; have to unlock record to avoid leak
if (ident != null)
{
persistenceContext.getRecordLockContext().forceRelease(ident, this);
}
}
// everything went fine ?
return currentRecord != null;
}
/**
* Validate the record, if any, currently in the buffer. If no record is available, return immediately.
*
* @param throwValidationException
* What to do in case the validation fails. If {@code true}, throw a {@code ValidationException}
* otherwise return {@code true} if validation is successful and {@code false} otherwise.
*
* @return <code>true</code> if validation passed OK.
*
* @throws ValidationException
* if the buffer fails validation.
* @throws ErrorConditionException
* if the buffer fails validation.
*/
public boolean validate(boolean throwValidationException)
throws ValidationException
{
if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && currentRecord.primaryKey() == null)
{
ErrorManager.recordOrThrowError(15991, getLegacyName());
// Attempt to create in multi-tenant table '<tablename>' where the partition (usually default) has not been allocated. (15991)
return false; // the records created by the default tenant are volatile, non persistable
}
if (!isAvailable())
{
return false;
}
Record dmo = getCurrentRecord();
if (dmo != null && dmo.isDirty() && !isTemporary())
{
maybeFireWriteTrigger();
}
try
{
validate(dmo);
return true;
}
catch (ValidationException exc)
{
if (throwValidationException)
{
reportValidationException(exc, this, false);
throw exc;
}
return false;
}
catch (PersistenceException exc)
{
if (throwValidationException)
{
ErrorManager.recordOrThrowError(exc);
throw new ValidationException(exc);
}
return false;
}
}
/**
* Perform a full validation (all unique constraints and non-nullable properties) of the given record.
*
* @param dmo
* DMO to be validated.
*
* @throws ValidationException
* if the DMO state fails any unique or non-nullable constraint check.
* @throws PersistenceException
* if there is any error gathering index metadata or a database error performing validation.
*/
void validate(Record dmo)
throws ValidationException,
PersistenceException
{
validateMaybeFlush(dmo, false, true);
}
/**
* Indicate whether a backing data record currently is available. This
* will only be the case if one has been created with {@link #create} or
* retrieved in a query.
*
* @return <code>true</code> if data is available, <code>false</code> if
* no record backs this buffer.
*/
protected boolean isAvailable()
{
return (currentRecord != null);
}
/**
* Checks if the record in the buffer has changed in database since the
* last <code>FIND CURRENT</code> or <code>GET CURRENT</code>.
* P2J implementation of 4GL function <code>CURRENT-CHANGED</code>.
*
* @return <code>true</code> if the database record differs from the
* value in memory buffer that was queried with the last
* <code>FIND</code> and <code>false</code> otherwise
*/
protected boolean isRecordChanged()
{
return recordChanged;
}
/**
* Checks if this buffer is a read-only buffer. A read-only buffer is created by the {@code
* DatabaseTriggerManager} when processing a WRITE event trigger with OLD buffer. It is also
* used as an optimization for tables which contain read-only data.
*
* @return {@code true} if the buffer can only contain read-only data.
*/
protected boolean isReadonly()
{
return readonly;
}
/**
* Indicate whether this buffer is a "fake" buffer. A "fake" buffer is one that is used as
* the OLD buffer when processing a write trigger. Fake buffers are always read-only. This
* method can be used to distinguish a read-only buffer which is used for a write trigger from
* a buffer that is read-only as a performance optimization.
*
* @return {@code true} if the buffer is a fake buffer, as described above.
*/
protected boolean isFake()
{
return fake;
}
/**
* Indicate whether the last record retrieval attempt was denied because
* the target record was locked.
*
* @return <code>true</code> if the record was locked; <code>false</code>
* if the attempt succeeded, or if this query has not yet
* attempted to retrieve a record.
*/
protected boolean wasLocked()
{
return locked;
}
/**
* Set the flag indicating whether the last record retrieval attempt
* failed because another user had the target record locked.
*
* @param locked
* New value of the flag.
*/
protected void setLocked(boolean locked)
{
this.locked = locked;
}
/**
* Indicate whether the buffer currently contains a record, and whether
* that record was newly created (as opposed to having been read from the
* database).
*
* @return <code>true</code> if record is newly created, else
* <code>false</code>.
*/
protected boolean isNewlyCreated()
{
return (currentRecord != null && newlyCreated);
}
/**
* Indicate whether the last record retrieval attempt failed because the
* query criteria was ambiguous.
*
* @return <code>true</code> if the query was ambiguous;
* <code>false</code> if the attempt succeeded, or if no attempt
* has yet been made.
*/
protected boolean wasAmbiguous()
{
return ambiguous;
}
/**
* Determines if the buffer is dynamic.
*
* @return <code>true</code> if it is a dynamic buffer.
*/
protected boolean isDynamic()
{
return dynamic;
}
/**
* Mark the buffer as dynamic.
*/
protected void setDynamic()
{
this.dynamic = true;
pm.processResource((WrappedResource) dmoProxy, true);
}
/**
* Mark the buffer as dynamic.
*/
protected void _setDynamic()
{
this.dynamic = true;
}
/**
* Set the flag indicating whether the last record retrieval attempt
* failed because the query criteria was ambiguous.
*
* @param ambiguous
* New value of the flag.
*/
protected void setAmbiguous(boolean ambiguous)
{
this.ambiguous = ambiguous;
}
/**
* Disable the buffer's default behavior of releasing its current record
* when the transaction manager invokes its {@link #iterate()} method.
* This is necessary for the outer buffers in a compound query.
*/
protected void disableReleaseOnIterate()
{
iterateReleaseOverrideScope = bufferManager.getOpenBufferScopes();
}
/**
* Indicate whether the record currently in the buffer represents a copy of
* a DMO from the dirty database. This happens in the case where another
* context inserts a new record, but has not committed it yet, and this
* context finds that record via dirty checking.
*
* @return <code>true</code> if current record is a dirty copy, else
* <code>false</code>.
*/
protected boolean isDirtyCopy()
{
return dirtyCopy;
}
/**
* Mark the buffer's current record as no longer transient by resetting it
* create scope to its default value.
*/
protected void markPersisted()
{
createScope = -1;
}
/**
* Add all property names in the given collection to the set of legacy key
* properties being monitored for foreign association synchronization
* purposes. Any change made to one or more of these properties should
* trigger a foreign key synchronization attempt.
*
* @param properties
* A collection of DMO property names.
*
* @see AssociationSyncher
* @see LocalSyncher
* @see InverseSyncher
*/
protected void addLegacyForeignKeys(Collection<String> properties)
{
if (legacyForeignKeys != null)
{
legacyForeignKeys.addAll(properties);
}
}
/**
* Get a set of dirty properties. Optionally the list can be filter to those properties in both
* dirty properties and those properties which participate in any indexes in the backing table.
*
* @param indexesOnly
* Filter only dirty properties that are part of index in backing table.
*
* @return A set with dirty property names. The set may be empty, but will not be {@code null}.
*/
BitSet getDirtyProperties(boolean indexesOnly)
{
if (currentRecord == null)
{
return new BitSet();
}
BitSet dps = currentRecord.getDirtyProps();
if (dps.isEmpty())
{
return new BitSet(); // regardless of [indexesOnly]
}
// be on safe side: make a copy:
dps = (BitSet) dps.clone();
if (indexesOnly)
{
BitSet allIndexedProperties = currentRecord._getRecordMeta().getAllIndexedProperties();
dps.and(allIndexedProperties);
}
return dps;
}
/**
* Indicate whether the given property participates in any index.
*
* @param propertyIndex
* Property name.
*
* @return {@code true} if {@code property} participates in an index, else {@code false}.
*/
protected boolean isPropertyIndexed(int propertyIndex)
{
checkActive();
if (indexedProperties == null)
{
indexedProperties = new BitSet();
indexedProperties.or(getIndexedProperties(false));
}
return indexedProperties.get(propertyIndex);
}
/**
* Get the buffer type hash key associated with this buffer.
*
* @return <code>BufferType</code> key associated with this buffer, or
* <code>null</code> if the buffer has not yet been initialized.
*/
protected BufferType getBufferType()
{
if (!isActive())
{
return null;
}
return bufferType;
}
/**
* Returns the value of a field in the record of this buffer.
*
* @param field
* The property name of the field.
*
* @return the value of the field
*/
protected BaseDataType getValue(String field)
{
if (currentRecord == null)
{
// maybe throw a runtime exception
return null;
}
Method method = dmoInfo.getPojoGetterMap().get(field);
if (method == null)
{
// maybe throw a runtime exception
return null;
}
PropertyMeta meta = dmoInfo.getPropsByGetterMap().get(method);
return meta.getDataHandler().getField(currentRecord, meta.getOffset());
}
/**
* Prepares for an immediate reload(). The current value of the record is saved temporarily
* in order to be available to compare to the new value that will be obtained in reload().
* This method is only called from within P2JQuery.current() implementation in order to
* prepare the buffer to recognise if the field has changed externally. After a call to
* this method and a reload() of the buffer, updateCurrentChanged() must be called to ensure
* the proper <code>recordChanged</code> settings and free of the temporary compare buffer.
*/
protected void armCurrentChanged()
{
if (currentRecord == null)
{
shadowCopyRecord = null;
return;
}
shadowCopyRecord = currentRecord.snapshot();
}
/**
* Direct access to the field value on the specified offset.
*
* @param offset
* The field offset in the record's datum.
*
* @return The field's value. This is not a BDT type, this will be a Java type.
*/
protected Object getDirectField(int offset)
{
return OrmUtils.getField(currentRecord, offset);
}
/**
* Direct assignment of a field, accessing the record's datum.
*
* @param value
* The field's value. This is not a BDT type, this will be a Java type.
* @param offset
* The field offset in the record's datum.
*/
protected void setDirectField(Object value, int offset)
{
Runnable restoreActiveState = null;
try
{
restoreActiveState = currentRecord.setActiveBuffer(RecordBuffer.this);
OrmUtils.setField(currentRecord, offset, value);
}
finally
{
if (restoreActiveState != null)
{
restoreActiveState.run();
}
}
}
/**
* Updates the <code>recordChanged</code> flag.
* This must be called right after an <code>armCurrentChanged()</code>followed by a
* <code>reload()</code>.
* This checks the current value stored in <code>currentRecord</code> against the value
* temporarily saved and updates the <code>recordChanged</code> accordingly. After that, the
* temp data is freed.
*/
protected void updateCurrentChanged()
{
if (this.shadowCopyRecord == null)
{
// something is wrong
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
Utils.describeContext() +
": armCurrentChanged() should be called before updateCurrentChanged()");
}
this.recordChanged = false;
return;
}
if (this.currentRecord == null)
{
// the record must have been deleted
this.recordChanged = false;
this.shadowCopyRecord = null;
return;
}
this.recordChanged = !this.currentRecord.equalData(shadowCopyRecord);
this.shadowCopyRecord = null;
}
/**
* Get the mappings of property names, from definition to bound buffer.
*
* @return Always <code>null</code>.
*/
protected Map<String, String> getBoundPropertyMappings()
{
return null;
}
/**
* Return the transaction helper associated with this buffer
*
* @return The transaction helper of this buffer
*/
protected TransactionManager.TransactionHelper getTxHelper()
{
return this.txHelper;
}
/**
* Return the change broker associated with this buffer
*
* @return The change broker of this buffer
*/
protected ChangeBroker getChangeBroker()
{
return this.changeBroker;
}
/**
* Return the procedure manager associated with this buffer
*
* @return The procedure manager of this buffer
*/
protected ProcedureManager.ProcedureHelper getProcedureManager()
{
return this.pm;
}
/**
* Get the record nursery of the current persistence context. If the context is not created yet,
* {@code null} is returned.
*
* @return Record nursery.
*/
protected RecordNursery getNursery()
{
return persistenceContext == null ? null : persistenceContext.getNursery();
}
/**
* Set the DMO implementation instance which backs this buffer with a another record, loaded from database.
* The buffer will help updating the new record, usually for internal, more complex processing.
* <p>
* This is a dedicated method accessible only from a very specific places within the persistence package.
* When the new record is set, the one eventually existing is released.
* <p>
* Note that the triggers are NOT fired when using this method.
*
* @param newCrtRecord
* DMO instance which represents the record currently backing this buffer. If {@code null}, the
* record (and possibly its corresponding lock) is released.
*
* @throws ErrorConditionException
* if there is an error transitioning a lock when releasing the current record; if there is an
* error validating or saving a transient record during flush.
*
* @see #setCurrentRecord(Record, boolean, boolean, boolean, boolean)
*/
public void loadRecord(Record newCrtRecord)
{
setCurrentRecord(newCrtRecord, false, false, false, false);
}
/**
* Sets whether buffer is part of buffer copy operation.
*
* @return <code>true</code> if it is part of BufferCopy operation, else <code>false</code>.
*/
void setBufferCopyDestination(boolean bufferCopyDestination)
{
this.bufferCopyDestination = bufferCopyDestination;
}
/**
* Add a scope where this buffer was registered with {@link BufferManager allBuffers}.
*
* @param scope
* The scope, as distance from the global scope.
*/
final void addAllBufferScope(int scope)
{
allBufferScopes.add(scope);
}
/**
* Add a scope where this buffer was registered with {@link BufferManager byLegacyName}.
*
* @param scope
* The scope, as distance from the global scope.
*/
final void addByLegacyNameScope(int scope)
{
byLegacyNameScopes.add(scope);
}
/**
* Add a scope where this buffer was registered with {@link BufferManager buffersByDMOBufClass}.
*
* @param scope
* The scope, as distance from the global scope.
*/
final void addBufferByClassScope(int scope)
{
buffersByDmoClassScopes.add(scope);
}
/**
* Add a scope where this buffer was registered with {@link BufferManager loadedBuffers}.
*
* @param scope
* The scope, as distance from the global scope.
*/
final void addLoadedBuffersScope(int scope)
{
loadedBuffersScopes.add(scope);
}
/**
* Add a scope where this buffer was registered with {@link BufferManager openBuffers}.
*
* @param scope
* The scope, as distance from the global scope.
*/
final void addOpenBuffersScope(int scope)
{
openBuffersScopes.add(scope);
}
/**
* Get the list of scopes in {@link #allBufferScopes}.
*
* @return See above.
*/
final int[] getAllBuferScopes()
{
return allBufferScopes.toArray();
}
/**
* Get the list of scopes in {@link #byLegacyNameScopes}.
*
* @return See above.
*/
final int[] getByLegacyNameScopes()
{
return byLegacyNameScopes.toArray();
}
/**
* Get the list of scopes in {@link #buffersByDmoClassScopes}.
*
* @return See above.
*/
final int[] getBuffersByDmoClassScopes()
{
return buffersByDmoClassScopes.toArray();
}
/**
* Get the list of scopes in {@link #loadedBuffersScopes}.
*
* @return See above.
*/
final int[] getLoadedBuffersScope()
{
return loadedBuffersScopes.toArray();
}
/**
* Get the list of scopes in {@link #openBuffersScopes}.
*
* @return See above.
*/
final int[] getOpenBuffersScope()
{
return openBuffersScopes.toArray();
}
/**
* Creates a new {@link Validation} instance using the passed arguments, and calls
* {@link Validation#validateMaybeFlush()}. Depending on the {@link Validation#wasFlushed()}
* state, it will {@link #reportChange report} the change so that listeners will be notified.
*
* @param dmo
* Record whose data is to be validated and optionally flushed to the database.
* @param flush
* {@code true} to validate and persist DMO's data; {@code false} to validate only.
* @param explicit
* {@code true} if this validation was explicitly requested by business logic; {@code false} if
* it is implicit/organic (e.g., in response to a record being updated).
*
* @return True if the validation passed.
*
* @throws PersistenceException
* if there was a database error during validation or data persistence. This
* generally will not be recoverable.
* @throws ValidationException
* if there was a validation error. This represents an application level data
* validation failure and is recoverable.
*/
protected boolean validateMaybeFlush(Record dmo, boolean flush, boolean explicit)
throws PersistenceException,
ValidationException
{
if (dmo == null)
{
return false;
}
// in case of multi-tenant without default tables, transient records CAN BE CREATED without PK,
// but they are NEVER validated/flushed, so neither they go to dirtyContext
if (dmoInfo.multiTenant &&
!dmoInfo.multiTenantWithDefault &&
persistenceContext.getTenantId() == TenantManager.DEFAULT_TENANT_ID)
{
return false;
}
boolean insert = dmo.checkState(DmoState.NEW);
Validation validation = new Validation(this,
dmo,
getMultiplexID(),
getLegacyName(),
persistenceContext.getNursery(),
uniqueTracker,
uniqueTrackerCtx,
flush,
!explicit);
try
{
validation.validateMaybeFlush();
}
catch (StaleRecordException exc)
{
// not a fatal error, but DMO is now marked STALE and cannot be saved or associated with the
// session cache
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
"stale record cannot be flushed: " + getEntityName() + "#" + dmo.primaryKey());
}
return false;
}
if (dmo == currentRecord && validation.wasFlushed())
{
if (insert)
{
markPersisted();
}
reportChange(dmo, insert, false);
}
return true;
}
/**
* Get the record metadata associated with this buffer's DMO class.
*
* @return Record metadata for the DMO.
*/
private RecordMeta getMetadata()
{
return currentRecord != null
? currentRecord._getRecordMeta() // quicker
: Session.getMetadata(dmoClass); // slightly slower
}
/**
* Check whether the given <code>fieldInfo</code> object has a buffer name qualifier, and if
* so, whether it matches the buffer's variable name or the dynamically assigned temp-table
* name (if this is the buffer for a dynamically prepared temp-table).
*
* @param fieldInfo
* Object which contains parsed field specifier information.
*
* @return <code>true</code> if there is no buffer qualifier, or if there is one and it has
* the expected value; <code>false</code> if there is a mismatched qualifier.
*/
private boolean checkBufferName(FieldInfo fieldInfo)
{
String bufName = fieldInfo.getBufferName();
return bufName == null ||
bufName.equalsIgnoreCase(variable) ||
bufName.equalsIgnoreCase(getDynamicName());
}
/**
* Check whether the given <code>fieldInfo</code> object has a buffer name qualifier, and if
* so, whether it matches the buffer's variable name. Report an error if there is a qualifier
* and it does not match the buffer's variable name.
*
* @param fieldInfo
* Object which contains parsed field specifier information.
* @param fieldSpec
* Field specifier string (used only to generate an error message).
*
* @return <code>true</code> if there is no buffer qualifier, or if there is one and it has
* the expected value; <code>false</code> if there is a mismatched qualifier, and we
* are in silent error mode.
*
* @throws ErrorConditionException
* if <code>fieldInfo</code> contains a mismatched buffer name qualifier and we are
* not in silent error mode.
*/
private boolean reportBadBufferName(FieldInfo fieldInfo, String fieldSpec)
{
if (checkBufferName(fieldInfo))
{
return true;
}
String spec = trimFieldSpec(fieldSpec, fieldInfo);
String msg = String.format("%s must be in a buffer in the data-source", spec);
ErrorManager.recordOrThrowError(11855, msg, false);
return false;
}
/**
* Report whether the given extent index is valid for the given property. Note that this
* requires that the buffer contains a record, since a method call is made to the backing DMO
* to determine the extent of <code>property</code>, if it represents an extent field.
*
* @param extentIndex
* A non-negative, zero-based array index to be tested for validity.
* @param property
* Name of the property to be checked.
*
* @return <code>true</code> if <code>property</code> represents an extent field and
* <code>extentIndex</code> is non-negative and is not out of bounds;
* <code>false</code> if <code>extentIndex</code> is negative or <code>property</code>
* does not represent an extent field, or <code>extentIndex</code> is out of bounds.
*/
private boolean isValidExtent(int extentIndex, String property)
{
if (extentIndex < 0)
{
return false;
}
Integer extent = dmoInfo.getExtentMap().get(property);
return (extent != null && extentIndex < extent);
}
/**
* Worker method which aggregates the values in the table backing this buffer.
*
* @param aggregate
* Name of FQL aggregate function (e.g., "count").
* @param column
* An expression indicating the name of the column to aggregate. May be the wildcard
* character ("*") for COUNT.
* @param num
* Variable into which to store the aggregate result.
* @param where
* FQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
private void aggregate(String aggregate, String column, NumberType num, String where, Object... args)
{
// TODO: handle parameter processing failure
AbstractQuery.preprocessSubstitutionArguments(bufferManager, () -> false, false, args);
initialize();
Persistence persistence = getPersistence();
String dmoImplName = getDMOImplementationName();
StringBuilder buf = new StringBuilder();
buf.append("select ");
buf.append(aggregate);
buf.append("(");
buf.append(column);
buf.append(") from ");
buf.append(dmoImplName);
try
{
if (where != null && !where.isEmpty())
{
FQLPreprocessor preproc = FQLPreprocessor.get(where,
getDatabase(),
getDialect(),
new RecordBuffer[] { this },
args,
null,
true,
null,
null,
false,
null);
buf.append(" where ");
buf.append(preproc.getFQL());
if (args != null && args.length > 0)
{
ParameterIndices pi = preproc.getParameterIndices();
int len = pi.getCount();
List<Object> argList = new ArrayList<>(len);
for (int i = 0, count = 0; count < len; i++)
{
Integer nextIndex = pi.getIndex(i);
if (nextIndex == null)
{
// original substitution parameter was inlined into FQL by preprocessor; skip it.
continue;
}
// substitution placeholders may have been reordered if this statement was
// rewritten by the FQL preprocessor; make sure we access the correct
// substitution parameter
Object next = args[nextIndex];
argList.add(next);
count++;
}
args = argList.toArray();
}
}
List<Object> list = persistence.list(new Class[] { getDMOImplementationClass() },
buf.toString(),
args,
1,
0,
null,
true);
NumberType result = null;
if (list != null)
{
Object value = list.get(0);
if (value instanceof Object[])
{
value = ((Object[]) value)[0];
}
if (value instanceof Number)
{
result = new decimal((Number) value);
}
else if (value instanceof NumberType)
{
result = (NumberType) value;
}
if (result == null)
{
// this only should happen if the embedded SQL aggregate expression in business
// logic is written incorrectly, so it is a sanity check but should not happen
// normally at runtime with correctly written/converted business logic
ErrorManager.throwError(new NumberedException("Wrong result type", 0));
// won't reach here with current implementation of EM.throwError(NE), but needed
// to satisfy compiler
return;
}
}
num.assign(result);
}
catch (PersistenceException e)
{
ErrorManager.recordOrThrowError(e);
}
}
/**
* Verify that the record buffer has been initialized. If not, raise an
* exception.
*
* @throws IllegalStateException
* if the buffer has not been initialized.
*/
private void checkActive()
{
if (!isActive())
{
throw new IllegalStateException("Buffer " + variable + " is not active");
}
}
/**
* Called by the transaction manager just before a buffer scope closes.
* This is our hook to update the state of locks and record availability.
* The current record is made unavailable and locks acquired during this
* scope are either:
* <ul>
* <li>released immediately, if this buffer scope never intersected a
* transaction's scope;
* <li>downgraded, if a transaction occurred within this buffer scope,
* but ended previous to this call; or
* <li>marked for deferred release, if we are still within the scope of
* an active transaction; the locks will be released at the end of
* the transaction.
* </ul>
*/
protected void finishedImpl()
{
if (openScopeCount == 0)
{
if (isActive())
{
// Notify connection manager that this buffer is done with the connection.
onCloseOutermostScope();
}
return;
}
// Decrement reference count of open scopes for this buffer.
openScopeCount--;
boolean debug = LOG.isLoggable(Level.FINE);
if (debug)
{
LOG.log(Level.FINE, describe() + ": finished block scope " + bufferManager.getOpenBufferScopes());
}
boolean globalScope = activeScopeDepth == 0;
// buffers associated with persistent procedures survive the closing of the outermost buffer scope;
// mark them as having "world" scope
if (globalScope)
{
worldScope = true;
}
boolean active = isActive();
if (active)
{
// Reset iterate release override scope.
iterateReleaseOverrideScope = -1;
// Perform cleanup if this is the outermost scope for this buffer
// (buffers can be shared and open scopes can be nested).
if (openScopeCount == 0)
{
cleanup();
}
}
// if the buffer belongs to a persistent ran procedure that is deleting, or if the buffer
// was opened in the global scope, deregister it as a record change listener
if (registeredWithGlobalScope == openScopeCount || (globalScope && openScopeCount == 0))
{
changeBroker.removeFromGlobal(this);
if (registeredWithGlobalScope == openScopeCount)
{
registeredWithGlobalScope = -1; // reset the flag
}
}
if (debug)
{
LOG.log(Level.FINE,
describe() +
": closed " +
(active ? "" : "in") +
"active buffer scope " +
bufferManager.getOpenBufferScopes());
}
}
/**
* Do clean-up in case the buffer runs out of (outermost) scope. This should be called from places
* responsible for the buffer lifecycle (e.g. {@link #finishedImpl()}).
* Its goal is to release any unwanted references that may leak or remain otherwise open.
*/
protected void cleanup()
{
activeScopeDepth = -1;
// attempt to flush a transient record in the buffer at the time its block scope ends; this will
// trigger validation and may report an error
if (isTransient())
{
try
{
flush(true);
}
catch (ValidationException exc)
{
reportValidationException(exc, this, false);
}
}
// The snapshot is no longer available
snapshot = null;
undoable = null;
// allow all locks pinned by this buffer to be released
if (pinnedLocks != null)
{
pinnedLocks.closeScope();
}
// relinquish any locks pinned by this buffer if outside a transaction
if (!bufferManager.isTransaction())
{
Set<Long> released = persistenceContext.getRecordLockContext().relinquishBufferLocks(this);
if (released != null)
{
bufferManager.reclaimPendingKeys(persistence, released);
}
}
// Notify connection manager that this buffer is done with the connection.
onCloseOutermostScope();
closeRelatedQueries();
// Make sure that the record we are trying to release is not actually a record that is being
// deleted. Because the converted 4GL trigger opens the scope of the buffer, it will
// release the record that is currently being deleted when the scope is closed.
if (currentRecord != null)
{
TriggerTracker tt = this.getTriggerTracker();
if (tt == null || !tt.isExecuting(DatabaseEventType.DELETE, currentRecord.primaryKey()))
{
// release the current record and allow the persistence to evict the record since it
// is not in use by this buffer
release(false, true);
}
}
}
/**
* Process one or more DMO property changes by creating or updating a
* <code>ReversibleUpdate</code> object and reporting the change to the
* {@link ChangeBroker} for further distribution to registered listeners.
*
* @throws PersistenceException
* if a problem occurs reporting the change to registered
* listeners.
*/
private void processChange()
throws PersistenceException
{
if (currentRecord != null && unreportedChanges == null)
{
return;
}
// if we are in a transaction, remember the scope depth at which this property change occurred
if (bufferManager.isTransaction())
{
markChangeScope();
}
reportChange(currentRecord, false, false);
}
/**
* Get the currently active temporary record.
*
* @return Current temporary record set into this buffer.
*
* @see #setTempRecord(Record)
*/
private Record getTempRecord()
{
if (tempRecords == null || tempRecords.isEmpty())
{
return null;
}
return tempRecords.get(tempRecords.size() - 1);
}
/**
* Return the handler associated with this buffer
*
* @return The handler of this buffer
*/
private Handler getHandler()
{
return this.handler;
}
/**
* Return the DMO class associated with this buffer
*
* @return The DMO class of this buffer
*/
private Class<?> getDMOClass()
{
return this.dmoClass;
}
/** Clears the unreported changes associated with this buffer. */
private void clearUnreportedChanges()
{
unreportedChanges = null;
}
/**
* Adds the information about new change to {@code unreportedChanges2} structure.
* @param propIndex
* The index of the property in the {@code Record.data} table.
* @param extent
* If this is an extent, the index of the property. Otherwise 0.
* @param diff
* The difference. Contains the values before and after.
*/
private void addUnreportedChanges(int propIndex, int extent, Object[] diff)
{
if (unreportedChanges == null)
{
unreportedChanges = new Object[metadata.getPropertyMeta(false).length][];
}
unreportedChanges[propIndex + extent] = diff;
}
/**
* Return the trigger tracker associated with this buffer
*
* @return The trigger tracker of this buffer
*/
protected TriggerTracker getTriggerTracker()
{
return this.triggerTracker;
}
/**
* Set the read only flag associated with this buffer
*/
private void setReadOnly(boolean readonly)
{
this.readonly = readonly;
}
/**
* Set the fake flag associated with this buffer
*/
private void setFake(boolean fake)
{
this.fake = fake;
}
/**
* Load the specified record, if any, from the database. If
* <code>lockType</code> is higher than the current lock type, it is
* upgraded immediately. If lower, it is downgraded immediately only if
* outside of a transaction. If inside a transaction, the lock is marked
* for downgrade to the specified type upon transaction end.
*
* @param target
* DMO to be reloaded. If <code>null</code>, an exception will be
* raised.
* @param lockType
* Requested lock type.
* @param undo
* <code>true</code> if currently processing an undo or
* <code>false</code> if currently processing a reload from database
* @param allowWriteTrigger
* {@code true} to allow a write trigger to be fired on this event, else {@code
* false}.
*
* @throws ErrorConditionException
* if there is no record loaded into the buffer and the error
* manager is not in silent mode.
*/
private void load(Record target, LockType lockType, boolean undo, boolean allowWriteTrigger)
{
if (target == currentRecord && undo && isTemporary)
{
// A temporary record cannot have been changed by another session,
// so there is no need to reload it in an undo situation if it is
// already loaded as the current record.
return;
}
LockType pinned = null;
Long id = null;
PersistenceException error = null;
try
{
if (target == null)
{
errorNotOnFile();
return;
}
if (!undo)
{
release(allowWriteTrigger);
}
id = target.primaryKey();
pinned = getPinnedLockType(id);
if (lockType == null)
{
lockType = pinned;
}
else if (!lockType.equals(pinned))
{
setPinnedLockType(id, lockType);
}
Record dmo = null;
boolean dirtyCopy = false;
if (dirtyContext != null && LockType.NONE.equals(lockType))
{
dmo = dirtyContext.getDirtyDMO(getEntityName(), id);
}
if (dmo != null)
{
dirtyCopy = true;
}
else if (target == currentRecord && (isTransient() || undo))
{
// if the target for reload is the same as the current record and the record either
// was never saved to the database or this is an undo operation (i.e., we are
// basically in a no-op reload, since the record has not changed (which we know
// because no dirty DMO was found)), simply reload the same record
dmo = target;
}
else
{
long timeout = persistence.getTimeOut();
dmo = persistence.load(this.dmoClass, id, lockType, timeout, true);
}
setCurrentRecord(dmo, undo, newlyCreated, dirtyCopy, false);
if (dmo != null && !LockType.NONE.equals(lockType))
{
// setCurrentRecord bypasses the registration of this buffer with the lock state
// managed by the record lock context, which can cause premature lock release under
// certain conditions, so we register the buffer here
RecordIdentifier<String> ident = dmo.getRecordLockIdentifier();
persistenceContext.getRecordLockContext().registerBuffer(ident, this);
}
}
catch (MissingRecordException exc)
{
// record must have been deleted in another context
setCurrentRecord(null, undo, false, false, false);
}
catch (LockUnavailableException exc)
{
locked = true;
error = exc;
}
catch (PersistenceException exc)
{
error = exc;
}
finally
{
if (error != null)
{
if (id != null && pinned != null)
{
setPinnedLockType(id, pinned);
}
ErrorManager.recordOrThrowError(error);
}
}
}
/**
* Set the DMO proxy which references this buffer instance.
*
* @param dmoProxy
* DMO proxy object.
*/
private void setDMOProxy(BufferReference dmoProxy)
{
this.dmoProxy = dmoProxy;
}
/**
* Set the DMO implementation instance which backs this buffer, or release the record currently
* stored in the buffer.
* <p>
* If a new DMO is set to this buffer, the {@code WRITE} trigger is fired and the existing one
* (if any) is flushed to backing database, its count usage decremented and, if its use count
* reaches zero, the old record is detached from the current Hibernate session.
* <p>
* If not {@code null}, the new DMO is registered with the current Hibernate session and its
* use count is incremented.
*
* @param newCrtRecord
* DMO instance which represents the record currently backing this buffer. If
* {@code null}, the record (and possibly its corresponding lock) is released.
* @param undo
* {@code true} if currently processing an undo, else {@code false}. The outgoing
* record, if any, is not validated and flushed if this parameter is {@code true}.
* @param newlyCreated
* {@code true} if the record being stored in the buffer is newly created.
* @param dirtyCopy
* Indicates whether {@code newCrtRecord} represents a copy of a DMO found in the
* dirty database.
* @param allowWriteTrigger
* {@code true} to allow a write trigger to be fired on this event, else {@code
* false}.
*
* @throws ErrorConditionException
* if there is an error transitioning a lock when releasing the current record; if
* there is an error validating or saving a transient record during flush.
*
* @see #release(boolean)
*/
private void setCurrentRecord(Record newCrtRecord,
boolean undo,
boolean newlyCreated,
boolean dirtyCopy,
boolean allowWriteTrigger)
{
setCurrentRecord(newCrtRecord, undo, newlyCreated, dirtyCopy, allowWriteTrigger, true);
}
/**
* Set the DMO implementation instance which backs this buffer, or release the record currently
* stored in the buffer.
* <p>
* If a new DMO is set to this buffer, the {@code WRITE} trigger is fired and the existing one
* (if any) is flushed to backing database, its count usage decremented and, if its use count
* reaches zero, the old record is detached from the current Hibernate session.
* <p>
* If not {@code null}, the new DMO is registered with the current Hibernate session and its
* use count is incremented.
*
* @param newCrtRecord
* DMO instance which represents the record currently backing this buffer. If
* {@code null}, the record (and possibly its corresponding lock) is released.
* @param undo
* {@code true} if currently processing an undo, else {@code false}. The outgoing
* record, if any, is not validated and flushed if this parameter is {@code true}.
* @param newlyCreated
* {@code true} if the record being stored in the buffer is newly created.
* @param dirtyCopy
* Indicates whether {@code newCrtRecord} represents a copy of a DMO found in the
* dirty database.
* @param allowWriteTrigger
* {@code true} to allow a write trigger to be fired on this event, else {@code
* false}.
* @param honorMultipleReferences
* Honor the flushing suppressing of the existing record if is also loaded in another buffer.
*
* @throws ErrorConditionException
* if there is an error transitioning a lock when releasing the current record; if
* there is an error validating or saving a transient record during flush.
*
* @see #release(boolean)
*/
private void setCurrentRecord(Record newCrtRecord,
boolean undo,
boolean newlyCreated,
boolean dirtyCopy,
boolean allowWriteTrigger,
boolean honorMultipleReferences)
{
if (dmoProxy != null)
{
// reset the DATA-SOURCE-MODIFIED
((Buffer) dmoProxy).setDataSourceModified(false);
}
if (newCrtRecord != null)
{
getTxHelper().checkTransaction(persistence.getDatabase(!dmoInfo.multiTenant).getId());
}
boolean sameRecord = areDMOsSame(this.currentRecord, newCrtRecord);
if (newCrtRecord != null)
{
sameRecord = sameRecord && this.dirtyCopy == dirtyCopy;
if (sameRecord)
{
if (!undo)
{
this.newlyCreated = newlyCreated;
if (snapshot != null && undoable != null)
{
undoable.checkUndoable(true);
}
snapshot = null;
}
bufferManager.notifyRecordWasLoaded(this);
// the old and new DMO can represent the same backing record, but still be different
// DMO instances; if they're the same instance, we're done here
if (currentRecord == newCrtRecord)
{
return;
}
}
}
// the ROW-UPDATE event is fired "immediately before the record is updated in the temp-tables"
// NOTE: because the way FWD handles transactions that moment is difficult to intercept. When a change
// operation occurred on the record, our validation has a secondary effect of flushing the
// record to database so at this moment the record is already updated and its flags set to CACHED
// and CHANGED flag is off. To detect if the record was indeed changed we test [isTouched] flag
maybeFireRowUpdateEvent();
// TODO: this is reported as unused
boolean clearValidationState = true;
try
{
// certain processing should not take place if we are resetting the current record due to an undo
// also, write trigger and flushing shouldn't happen if the record is loaded in another buffer as well
boolean singleLoadedBuffer = (currentRecord == null || !currentRecord.isMultiplyReferenced(false));
if (!undo && (!honorMultipleReferences || singleLoadedBuffer))
{
// if a different buffer created this record, use it to handle write trigger and flushing duties
RecordBuffer targetBuffer = (creatingBuffer != null ? creatingBuffer : this);
// DO NOT flush or fire WRITE trigger for empty or readonly buffers
// DO NOT flush or fire WRITE trigger if the current record is not changed
boolean doFlush = false;
// if the WRITE trigger is already executing (down the stack), do not re-fire it again;
// the record will be flushed when that trigger ends.
// DO NOT fire WRITE trigger for readonly buffers
// DO NOT fire WRITE trigger if the current record is not changed, even if transient
if (isAvailable() && !isReadonly())
{
boolean currentRecordChanged = currentRecord.checkState(DmoState.CHANGED) &&
(targetBuffer.getTriggerTracker() == null ||
!targetBuffer.getTriggerTracker().isExecuting(DatabaseEventType.WRITE,
currentRecord.primaryKey()));
if (currentRecordChanged)
{
boolean maybeChanged = allowWriteTrigger && targetBuffer.maybeFireWriteTrigger();
if (maybeChanged && shadowCopyRecord != null)
{
// this is a bit odd, but the shadow copy needs to be refreshed if the trigger
// was executed because it might have been changed. [shadowCopyRecord] is used
// to check an external change. Trigger changes are NOT considered external.
armCurrentChanged();
}
doFlush = true;
}
// if transient, JUST flush (otherwise the record is lost)
else if (currentRecord.checkState(DmoState.NEW))
{
doFlush = true;
}
}
if (doFlush)
{
// flush any previously created record to the database before we release/replace it, unless
// it is already marked invalid
targetBuffer.flush(false);
// if a newly created record is being replaced via a query, do not carry the
// createScope setting forward. This only matters if the flush above was not clean
if (!newlyCreated && !sameRecord)
{
targetBuffer.markPersisted();
}
}
}
}
catch (ValidationException exc)
{
if (!isUndoable())
{
// if not in silent error mode, we will exit abnormally with an error condition
// exception from reportValidationException below, and we want to leave the validation
// state intact for the next validation cycle; otherwise, we will continue and change
// or clear the record in the buffer, so we want to clear the validation state to
// prevent it from being out of sync with the buffer's record
clearValidationState = txHelper.errHlp.isSilent();
}
reportValidationException(exc, this, false);
}
finally
{
if (dirtyCopy && newCrtRecord != null && dirtyContext != null)
{
RecordIdentifier<String> ident = new RecordIdentifier<>(getEntityName(),
newCrtRecord.primaryKey());
creatingBuffer = dirtyContext.getCreatingBuffer(ident);
}
else
{
creatingBuffer = null;
}
}
if (this.currentRecord != null)
{
// store placeholder record if releasing current record and we haven't done so previously
if (snapshot == null && (newCrtRecord == null || newlyCreated))
{
if (undoable != null)
{
undoable.checkUndoable(true);
}
snapshot = this.currentRecord;
}
// transition lock when releasing current record
// the code below corresponds to case: newCrtRecord == null || !sameRecord
try
{
if (!sameRecord)
{
// Try to release or downgrade the lock, if any.
if (bufferManager.isTransaction())
{
Long id = this.currentRecord.primaryKey();
LockType pinned = getPinnedLockType(id);
if (pinned.isExclusive())
{
// Downgrade pinned lock type. This won't actually downgrade the lock, it
// just lets the lock context know that this buffer is done with its
// exclusive lock on this record.
setPinnedLockType(id, LockType.SHARE);
}
}
else
{
// try to release the lock
RecordIdentifier<String> ident = this.currentRecord.getRecordLockIdentifier();
persistenceContext.getRecordLockContext().lock(ident, LockType.NONE, 0L, this);
}
}
}
catch (LockUnavailableException exc)
{
// safe to ignore, will not be thrown by lock release
}
finally
{
if (this.currentRecord != newCrtRecord && undoable != null)
{
this.undoable.checkUndoable(true);
}
this.isTouched = false;
// invalidate CURRENT-CHANGED flag. it will be re-set at the end of
// armCurrentChanged/updateCurrentChanged bracketing if the case
this.recordChanged = false;
// make sure the current record is updated, if an exception was thrown above
Record oldRecord = this.currentRecord;
this.currentRecord = newCrtRecord;
if (!isTemporary)
{
TriggerTracker.releaseTracker(triggerMap, oldRecord);
this.triggerTracker = TriggerTracker.getTracker(triggerMap,
this.currentRecord,
dmoBufInterface,
bufferManager,
dbTrigManager);
}
else if (oldRecord.checkState(DmoState.NEW) && undo)
{
try
{
// this is important to dealloc the reserved PK
DirectAccessHelper.clearPrimaryKey(persistence.getSession(Persistence.TEMP_CTX),
(TempRecord) oldRecord);
}
catch (DirectAccessException e)
{
LOG.log(Level.SEVERE, "Failed deleting the transient record on undo", e);
}
}
updateNoUndoState();
// make sure to notify bufferManager after the record was loaded
bufferManager.notifyRecordWasLoaded(this);
// don't count usages of template records and don't register with persistence
if (oldRecord.primaryKey() != null && oldRecord.primaryKey() > 0)
{
decrementDMOUseCount(oldRecord, false);
}
}
}
// update current record
if (this.currentRecord != newCrtRecord && undoable != null)
{
this.undoable.checkUndoable(true);
}
if (!isTemporary)
{
TriggerTracker.releaseTracker(triggerMap, currentRecord);
this.triggerTracker = TriggerTracker.getTracker(triggerMap,
newCrtRecord,
dmoBufInterface,
bufferManager,
dbTrigManager);
}
this.currentRecord = newCrtRecord;
this.recordChanged = false; // invalidate CURRENT-CHANGED flag (see above)
this.isTouched = false;
this.newlyCreated = newlyCreated;
this.dirtyCopy = dirtyCopy;
updateNoUndoState();
bufferManager.notifyRecordWasLoaded(this);
// don't count usages of template records and don't register with persistence
if (newCrtRecord != null && newCrtRecord.primaryKey() != null && newCrtRecord.primaryKey() > 0)
{
incrementDMOUseCount(newCrtRecord, false);
}
// if not null and not currently processing an undo, reset certain state
if (newCrtRecord != null && !undo)
{
locked = false;
unknownMode = false;
// creating a new record does not change our placeholder snapshot
if (!newlyCreated)
{
if (snapshot != null && undoable != null)
{
undoable.checkUndoable(true);
}
snapshot = null;
}
if (LOG.isLoggable(Level.FINEST))
{
String desc = describe();
LOG.log(Level.FINEST, desc + ": currentRecord = " + this);
LOG.log(Level.FINEST, desc + ": snapshot = " + toString(snapshot));
}
}
if (newlyCreated)
{
UserTableStatUpdater.create(getDatabase(), this);
}
}
/**
* Checks whether the conditions for firing ROW-UPDATE event are met. If affirmative the event is triggered
* and the dataset's callback is invoked with a handle to this buffer as parameter.
*/
private void maybeFireRowUpdateEvent()
{
if (isTouched && isTemporary && this.currentRecord != null)
{
BufferImpl buf = (BufferImpl) dmoProxy;
if (buf._dataSet() != null)
{
// checks whether tracking is active
if (getParentTable()._isTrackingChanges())
{
buf.invokeCallback(Buffer.ROW_UPDATE_EVENT);
ErrorManager.clearPending(); // if an error is raised in this callback it is simply ignored
}
}
}
}
/**
* Store the undoability of this buffer in the record's state. Even though the same record
* may be stored in multiple buffers at the same time, this is safe because (1) a record
* instance is unique to a user session, and (2) buffers backed by the same table must have
* the same undoability state.
*/
private void updateNoUndoState()
{
if (currentRecord != null)
{
currentRecord.updateState(null, DmoState.NOUNDO, !isUndoable());
}
}
/**
* Indicate whether a commit is pending for the most recent changes made
* to the current record.
*
* @return <code>true</code> if a commit is pending, else
* <code>false</code>.
*/
private boolean isCommitPending()
{
return commitPending;
}
/**
* Set the commit pending state.
*
* @param commitPending
* <code>true</code> if we are currently in batch assign mode and
* the changes made while in this mode should be committed to the
* database immediately upon completing this mode;
* <code>false</code> if the commit pending state should be reset
* such that no commit should occur.
*/
private void setCommitPending(boolean commitPending)
{
this.commitPending = commitPending;
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @param buffer
* The legacy buffer name.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
private static void errorNotAvailable(String buffer)
{
String msg = "No " + buffer + " record is available";
if (LOG.isLoggable(Level.FINE))
{
PersistenceException exc = new PersistenceException(msg, 91);
LOG.log(Level.FINE, msg, exc);
}
ErrorManager.recordOrThrowError(91, msg);
}
/**
* Record or throw a "record not available" error condition for
* the current DMO type.
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
private void errorNotAvailable()
{
// TODO execute "no record is available" triggers ?
errorNotAvailable(getLegacyName());
}
/**
* This class manages the record lock types pinned by this buffer. This information is used by
* {@link RecordLockContext} to manage the communal locks held by the current user session.
* <p>
* When outside a transaction, only the lock type for the current record held by the buffer, if
* any, is tracked. It only can be a SHARE lock or none at all.
* <p>
* When inside a transaction, the most restrictive lock type for every record which passes through
* the buffer is tracked. Once a lock is acquired, it is not downgraded or released until the
* transaction ends.
*/
private class PinnedLocks
{
/** The pinned lock type for the current record, if any, when not in a transaction */
private LockType noTrans = null;
/** The pinned lock types for all records used by this buffer in the current transaction */
private HashMap<Long, LockType> inTrans = new HashMap<>();
/** Transaction count at last check, used for comparison to see if we are in a different transaction */
private long lastXactCount = -1L;
/**
* Get the pinned lock type for the record with the given primary key.
*
* @param id
* Primary key.
*
* @return Lock type associated by this buffer with the given primary key.
*/
LockType getPinnedLockType(Long id)
{
boolean transaction = manageTransactionState();
if (!transaction)
{
return noTrans;
}
return inTrans.get(id);
}
/**
* Set the pinned lock type for the record with the given primary key.
*
* @param id
* Primary key.
* @param lockType
* Lock type to set.
*/
void setPinnedLockType(Long id, LockType lockType)
{
boolean transaction = manageTransactionState();
LockType waitLockType = lockType == LockType.NONE ? null : lockType.toWaitVariant();
if (!transaction)
{
noTrans = currentRecord != null && id == currentRecord.primaryKey() ? waitLockType : null;
return;
}
// remember the most restrictive lock type for the given primary key
inTrans.compute(id, (k, v) ->
{
if (v == null || waitLockType == null)
{
// add missing entry (v == null) or remove entry (waitLockType == null)
return waitLockType;
}
if (waitLockType.compareTo(v) > 0)
{
// replace existing entry with more restrictive lock type
v = waitLockType;
}
return v;
});
}
/**
* Clean up when the buffer closes its outermost scope.
*/
void closeScope()
{
noTrans = null;
inTrans.clear();
}
/**
* Manage the internal state of this object, based on the current application transaction state.
*
* @return {@code true} if an application transaction is active, else {@code false}.
*/
private boolean manageTransactionState()
{
boolean blockInTransaction = bufferManager.isTransaction();
boolean rollingBack = blockInTransaction ? bufferManager.getTxHelper().hadRollback() : false;
// treat rollback in progress as out of transaction
boolean transaction = blockInTransaction && !rollingBack;
long xactCount = bufferManager.getTransactionCount();
if ((!inTrans.isEmpty()) == transaction && xactCount == lastXactCount)
{
return transaction;
}
lastXactCount = xactCount;
Long id = currentRecord != null ? currentRecord.primaryKey() : null;
if (transaction)
{
// add entry to (otherwise empty) inTrans map for current record and lock type, if any
if (noTrans != null && id != null)
{
inTrans.put(id, noTrans);
}
// dont clean noTrans lock in order to be able to restore it
//noTrans = null;
}
else
{
// remember lock type for current record, if any, downgrading from exclusive to share if needed
// (can't hold exclusive lock outside a transaction)
if (id != null)
{
LockType lt = inTrans.get(id);
// downgrade noTrans lock only if not in rollback, otherwise restore it
if (lt != null && !rollingBack)
{
noTrans = lt.isExclusive() ? LockType.SHARE : lt;
}
}
inTrans.clear();
}
return transaction;
}
}
/**
* Handler for methods invoked on the DMO proxy provided to client code.
* Methods handled here include all methods of the DMO interface used to
* create the proxy, as well as the <code>buffer()</code> method which
* permits classes in this package to retrieve the record buffer instance
* itself from the proxy. The former methods are meant for use by client
* code, while the latter allows persistence support classes, such as
* query objects, to access the buffer for implementation and housekeeping
* purposes.
*/
private final class Handler
implements InvocationHandler
{
/** Exception thrown during validation of no-undo temp-tables, which must be deferred */
private ValidationException deferredValidationError = null;
/**
* Default constructor.
*/
Handler()
{
}
/**
* This method intercepts proxied method calls invoked on DMOs. This includes all property getter and
* setter methods, as well as methods declared by {@code Object} and {@code BufferReference}.
*
* @param proxy
* The target proxy object.
* @param method
* The method to be executed.
* @param args
* The method arguments.
*
* @return The method invocation result.
*
* @throws Throwable
* For various reasons, like illegal access to fields, illegal arguments or target.
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
Object result = null;
boolean isSetter = false;
boolean isDirtyRead = false;
Runnable restoreActiveState = null;
String property = null; // TODO: remove this, use the property index
PropertyMeta pm = null;
boolean isCLOBGetter = false;
try
{
// handle reserved methods
Class<?> declClass = method.getDeclaringClass();
if (declClass.equals(Object.class))
{
return Utils.invoke(method, RecordBuffer.this, args);
}
else if (declClass.equals(BufferReference.class))
{
switch (method.getName())
{
case "buffer": return RecordBuffer.this;
case "definition": return RecordBuffer.this;
}
return Utils.invoke(method, RecordBuffer.this, args);
}
// TODO: use something more reliable (but efficient) here to determine whether this is a setter
isSetter = (method.getReturnType() == Void.TYPE);
if (isSetter)
{
if (!isTemporary && !bufferManager.isTransaction())
{
ErrorManager.recordOrThrowError(7369,
"Update of buffer field object requires a transaction",
false);
return null;
}
// in case of multi-tenant without default tables, transient records CAN BE CREATED without PK,
// but they are NEVER validated/flushed, so neither they go to dirtyContext
if (!dmoInfo.multiTenant ||
dmoInfo.multiTenantWithDefault ||
persistenceContext.getTenantId() != TenantManager.DEFAULT_TENANT_ID)
{
// start tracking current record in DirtyContext
if (dirtyContext != null && DirtyShareSupport.isEnabledCrossSession())
{
dirtyContext.insert(RecordBuffer.this, getCurrentRecord(), true, null);
}
bufferManager.registerDirtyBuffer(RecordBuffer.this);
}
}
// if another buffer created the current record, delegate setter method calls to it,
// to ensure all validation and flushing is tracked properly
if (creatingBuffer != null && currentRecord != null && isSetter)
{
// prevent recursive call
if (!creatingBuffer.handler.equals(this))
{
return creatingBuffer.handler.invoke(proxy, method, args);
}
}
initialize();
// is property an extent field?
boolean extent = false;
if (args != null)
{
extent = (args.length == (isSetter ? 2 : 1));
}
// we may be in temporary record mode to allow client-side where clause processing
Record tempRecord = getTempRecord();
// if a temporary record is set, use it to back the invocation
if (tempRecord != null)
{
return Utils.invoke(method, tempRecord, args);
}
if (isSetter)
{
// check whether we are allowed to make changes to this buffer
BufferImpl buf = (BufferImpl) proxy;
if (isTemporary && buf.isBeforeBuffer() && buf.hasBeforeBufferConstraints())
{
ErrorManager.recordOrThrowError(12375, buf.name().toStringMessage());
// Cannot update fields in a BEFORE-TABLE.
return null;
}
// if a setter is called, ensure the transactionId is set for its database
getTxHelper().checkTransaction(persistence.getDatabase(!dmoInfo.multiTenant).getId());
}
// ensure backing DMO is available
if (currentRecord == null)
{
// TODO: getters map is going away...
boolean validGetter = getters.contains(method);
// outer join processing will set unknown mode, because in an outer join situation,
// it can be valid to have no record in the buffer and still request a property
// value. The same stands when query parameters are evaluated.
// Return unknown value in these cases.
if ((unknownMode || bufferManager.isUnknownMode()) && validGetter)
{
return FieldReference.unknownValue(method);
}
else
{
// throw an exception if not in silent mode
if (isSetter)
{
// attempt to set field on empty buffer results in "unable to update" error
ErrorManager.recordOrThrowError(142, "Unable to update " + getLegacyName() + " Field");
}
else
{
// attempt to get field from empty buffer results in "record not available" error
errorNotAvailable();
}
// silent error mode; return unknown value if possible
return (validGetter ? FieldReference.unknownValue(method) : null);
}
}
boolean denormalized = false;
boolean activeBufferSet = bufferManager.isBufferDirtyBatch(RecordBuffer.this);
if (isSetter)
{
// for any setter method and some getters (e.g., _getClob), the DMO needs access to this buffer
// the active buffer is set for each buffer is a single assign and just once for a buffer
// used in a batch assign.
if (!bufferManager.isBatchAssignMode() || !activeBufferSet)
{
restoreActiveState = currentRecord.setActiveBuffer(RecordBuffer.this);
}
pm = dmoInfo.getPropsBySetterMap().get(method);
if (pm == null)
{
LOG.log(Level.SEVERE, "Failed to obtain property for " + method);
return null; // TODO: better idea?
}
// in case of dynamic index of denormalized extent field, the [pm] is just the 'generic'
// PropertyMeta (the first element, see DmoMeta.getPropsBySetterMap()) we need to find the true
// property now
if (!pm.getOriginalName().isEmpty() && args.length > 1)
{
int extIdx = (int) args[0]; // already computed as 0-base
pm = metadata.getPropertyMeta(false)[pm.getOffset() + extIdx];
denormalized = true;
if (extIdx < 0 || extIdx >= pm.getDmoProperty().extent)
{
String msg = "Array subscript " + (extIdx + 1) + " is out of range";
ErrorManager.recordOrThrowError(26, msg);
return null;
}
}
property = pm.getName();
isDirtyRead = dmoInfo.isDirtyRead();
// if this is a read-only mode buffer, do not allow this to happen
if (readonly)
{
int[] errCodes = { 2665, 142 };
String[] errMessages =
{
"Attempt to update a Read-Only buffer for table " + variable,
"Unable to update " + getLegacyName() + " Field"
};
// TODO: this seems like an always fatal error, cannot be put on hold with no-error
ErrorManager.recordOrThrowError(errCodes, errMessages, true);
}
// if a template record is loaded we are not allowed to alter it that easy
Long pk = currentRecord.primaryKey();
if (pk != null && pk < 0)
{
final String msg = "Cannot update a template record without a schema lock";
ErrorManager.recordOrThrowError(12376, msg, false, false);
return null;
}
// setting an unknown value to a mandatory field will raise errors 275 and 142
// regardless we are in a batch or not
Object val = extent ? args[1] : args[0];
if (!(val instanceof BaseDataType))
{
if (LOG.isLoggable(Level.WARNING))
{
// is this legal?
LOG.log(Level.WARNING, "Attempt to assign a non - BDT value to a record field");
}
}
else
{
boolean batchAssign = bufferManager.isBatchAssignMode();
BaseDataType newBdtValue = (BaseDataType) val;
if (!(batchAssign || bulkCopy) && newBdtValue.isUnknown() && pm.isMandatory())
{
int[] errCodes = { 275, 142 };
String[] errMessages =
{
pm.getLegacyName() + " is mandatory, but has a value of ?",
"Unable to update " + getLegacyName() + " Field"
};
ErrorManager.recordOrThrowError(errCodes, errMessages, true);
}
}
// TODO: we need to copy the buffer to the before-table as it exists, but what if validation
// fails bellow?
// update the before table only when current (after) buffer is a dataset buffer
// has a BEFORE-TABLE to keep the changes in and tracking is activated (not in FILL mode)
BufferImpl after = (BufferImpl) RecordBuffer.this.dmoProxy;
if (!ignoreBeforeTracking &&
after.isAfterBuffer() &&
((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) after))._isTrackingChanges())
{
BufferImpl before = after.beforeBufferNative();
if (before != null) // should not be
{
rowid beforeRowid = peerRowid();
if (beforeRowid.isUnknown())
{
// create new before record image,
before.setUpBeforeBuffer(true);
before.create();
before.bufferCopy(after);
// then update my before-rowid and row-state,
before.buffer().rowState(Buffer.ROW_MODIFIED);
after.buffer().rowState(Buffer.ROW_MODIFIED);
before.buffer().peerRowid(after.rowID());
after.buffer().peerRowid(before.rowID());
// keep the reference to the datasource-rowid / origin-rowid,
((TemporaryBuffer) before.buffer()).getCurrentRecord()._originRowid(
((TemporaryBuffer) after.buffer()).getCurrentRecord()._originRowid());
((TemporaryBuffer) before.buffer()).getCurrentRecord()._datasourceRowid(
((TemporaryBuffer) after.buffer()).getCurrentRecord()._datasourceRowid());
before.buffer().flush();
before.setUpBeforeBuffer(false);
// do not release the before buffer, let it hold the newly created row
}
// else actually do nothing, the before image is already saved
}
}
}
else
{
pm = dmoInfo.getPropsByGetterMap().get(method);
if (pm == null)
{
LOG.log(Level.SEVERE, "Failed to obtain property for " + method);
return null;
}
if (pm.getDmoProperty()._fwdType.equals(clob.class))
{
restoreActiveState = currentRecord.setActiveBuffer(RecordBuffer.this);
isCLOBGetter = true;
}
if (extent && !pm.getOriginalName().isEmpty())
{
int extIdx = (int) args[0];
pm = metadata.getPropertyMeta(false)[pm.getOffset() + extIdx];
denormalized = true;
if (extIdx < 0 || extIdx >= pm.getDmoProperty().extent)
{
String msg = "Array subscript " + (extIdx + 1) + " is out of range";
ErrorManager.recordOrThrowError(26, msg);
return FieldReference.unknownValue(method);
}
}
}
// get the current value. It will become the [old] value in the trigger, if fired
BaseDataType oldVal = null;
if (isSetter &&
!extent &&
triggerTracker != null &&
triggerTracker.isTriggerEnabled(DatabaseEventType.ASSIGN, property, logicalDatabase))
{
// get the existing value and save it for calling the triggers
Method getter = dmoInfo.getPojoGetterMap().get(property);
PropertyMeta meta = dmoInfo.getPropsByGetterMap().get(getter);
oldVal = meta.getDataHandler().getField(currentRecord, meta.getOffset());
}
// we are about to change a field's value: take a snapshot the current values of the fields of the
// record to be use later, as the old DMO, if we decide later that a write-trigger will be fired
Record writeSnapshot = null;
if (isSetter &&
!extent &&
triggerTracker != null &&
triggerTracker.getOldDMO() == null &&
triggerTracker.isTriggerEnabled(DatabaseEventType.WRITE, null, logicalDatabase))
{
writeSnapshot = currentRecord.snapshot();
}
// invoke the method on the concrete DMO implementation
result = Utils.invoke(method, currentRecord, args);
if (writeSnapshot != null && !currentRecord.checkState(DmoState.CHANGED))
{
writeSnapshot = null; // actually no, the setter has not made any change to the property
}
boolean changed = false;
boolean processChange = false;
boolean batchAssign = false;
if (isSetter)
{
isTouched = true;
Object[] diff = null;
batchAssign = bufferManager.isBatchAssignMode();
int ormIndex = pm.getOffset();
// NOTE: if the property was denormalized, the property index [ormIndex] already contains
// the extent index
int extentIndex = extent && !denormalized ? (Integer) args[0] : 0;
diff = currentRecord.getActiveUpdateDiff(ormIndex, extentIndex);
if (batchAssign || bulkCopy)
{
// batch mode: defer validation until assignment batch is closed
// bulkCopy mode: skip validation (unless also in batch mode)
if (batchAssign && !activeBufferSet)
{
bufferManager.addDirtyBatchBuffer(RecordBuffer.this, restoreActiveState);
}
}
else
{
try
{
if (!validateMaybeFlush(currentRecord, false, false))
{
diff = null;
}
}
catch (ValidationException exc)
{
if (isUndoable())
{
currentRecord.undoActive();
throw exc;
}
else
{
// for no-undo buffers, we defer the exception, to allow the DMO to
// be updated with the incorrect value, to ensure future validation
// prevents this record from being flushed to the database; it is
// thrown near the end of invoke()
deferredValidationError = exc;
}
}
processChange = true;
}
changed = diff != null;
if (changed)
{
addUnreportedChanges(ormIndex, extentIndex, diff);
}
}
// decide whether the ASSIGN trigger will be invoked:
// * don't invoke the trigger if value hasn't changed
// * triggers do not operate on extent fields
boolean invokeTriggers = triggerTracker != null && changed && !extent;
boolean hasAssignTrigger = invokeTriggers &&
triggerTracker.isTriggerEnabled(DatabaseEventType.ASSIGN,
property,
logicalDatabase);
if (hasAssignTrigger && triggerTracker.isFieldInTrigger(property))
{
// we avoid recursive double triggering
if (triggerTracker.isFieldCurrentTrigger(property))
{
// block direct recursion; field is allowed to be changed inside its trigger
invokeTriggers = false;
}
else
{
// this is an indirect recursive call
Class<? extends DataModelObject> dmoIface = getDMOInterface();
DatabaseTriggerManager.handleError(
1003,
DatabaseEventType.ASSIGN,
dmoInfo.legacyTable,
TableMapper.getLegacyFieldName(dmoIface, property));
}
}
// we changed a field's value, archive the snapshot of the record to use later as the old DMO, if
// there is a WRITE trigger registered for this table
if (invokeTriggers && writeSnapshot != null)
{
triggerTracker.setOldDMO(writeSnapshot, bufferManager.getOpenBufferScopes());
}
// invoke assign triggers
if (invokeTriggers && hasAssignTrigger)
{
if (batchAssign) // always false ?
{
// defer firing of assign triggers in batch mode until batch is finished
triggerTracker.addPendingBatchField(property, oldVal);
}
else
{
boolean ok;
try
{
triggerTracker.pushField(property);
ok = DatabaseTriggerManager.triggerAssign(getDMOBufInterface(),
(Buffer) getDMOProxy(),
property,
oldVal,
logicalDatabase);
}
finally
{
String top = triggerTracker.popField();
assert property.equals(top); // the property must be the last one pushed
}
if (!ok)
{
// the assign trigger vetoed: rolling back the change
Utils.invoke(method, currentRecord, oldVal);
processChange = false;
}
}
}
// broadcast notification if state changed
if (processChange)
{
processChange();
}
if (deferredValidationError != null)
{
throw deferredValidationError;
}
}
catch (ValidationException exc)
{
// this is a 3rd time validation, final, on SQL server side
reportValidationException(exc, RecordBuffer.this, true);
// when a field validation error occurs (e.g., 32-bit integer overflow) the exception
// thrown is an ErrorConditionException, which is the target for an
// InvocationTargetException, so it is caught below
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
catch (InvocationTargetException exc)
{
// look for ErrorConditionException
Throwable ex2 = exc;
while (ex2 != null && !(ex2 instanceof ConditionException))
{
ex2 = ex2.getCause();
}
if (ex2 != null)
{
ErrorManager.recordOrThrowError(142, "Unable to update " + getLegacyName() + " Field");
}
else
{
DBUtils.handleException(getDatabase(), exc);
Throwable cause = exc.getTargetException();
if (LOG.isLoggable(Level.SEVERE))
{
String errorText = describe() + ": error invoking proxied DMO method";
LOG.log(Level.SEVERE, errorText, exc);
}
// must be the result of a programming error; don't wrap in a legacy-style
// exception type
throw cause;
}
}
finally
{
deferredValidationError = null;
// do not restore the active state for the buffers that were used in a batch assign,
// it is done in cleanupBatchMode().
if (restoreActiveState != null && (!bufferManager.isBatchAssignMode() || isCLOBGetter))
{
restoreActiveState.run();
}
}
return result;
}
}
/**
* Cross reference dirty DMO properties with indexes containing them, creating a list of indexes which have
* been updated.
*
* @param unique
* If {@code true}, only unique indexes are checked. If {@code false}, only non-unique indexes are
* checked.
* @param fullMatch
* If {@code true} every property in an index must be dirty for that index to be considered dirty.
* If {@code false}, any dirty property in an index is enough to consider that index dirty.
* @param cumulative
* Only in the case of non-unique indexes. If {@code true}, collect all indexes that were marked
* dirty since the record was loaded. If {@code false}, only indexes marked dirty since the last
* validation/share.
*
* @return A list of names of indexes considered dirty, in ascending alphabetical order (?).
*/
List<String> listDirtyIndexes(boolean unique, boolean fullMatch, boolean cumulative)
{
RecordMeta recordMeta = currentRecord._getRecordMeta();
BitSet[] indexSet = unique ? recordMeta.getUniqueIndices() : recordMeta.getNonuniqueIndices();
String[] indexNames = unique ? recordMeta.getUniqueIndexNames() : recordMeta.getNonuniqueIndexNames();
BitSet crtDirty = currentRecord.getDirtyProps();
List<String> ret = new ArrayList<>();
for (int k = 0; k < indexSet.length; k++)
{
if (fullMatch)
{
// all properties of an index are required to be dirty
BitSet indexCopy = (BitSet) indexSet[k].clone(); // make a copy as the [and] op is destructive
indexCopy.and(crtDirty);
if (cumulative)
{
indexCopy.clear();
}
if (indexCopy.equals(indexSet[k]))
{
ret.add(indexNames[k]);
}
}
else
{
// check if at least one property is dirty
if (crtDirty.intersects(indexSet[k]))
{
ret.add(indexNames[k]);
}
}
}
return ret;
}
/**
* A lightweight implementation of the <code>Undoable</code> interface
* which restores to the record buffer the current record and snapshot
* which were active at the time of backup.
* <p>
* An instance of this class is registered for undo support with the
* <code>TransactionManager</code> during record buffer construction.
*/
protected final class LightweightUndoable
extends LazyUndoable
{
/** DMO which is active in record buffer at the time of backup */
private Record activeRecord = null;
/** Snapshot which is active in record buffer at the time of backup */
private Record activeSnapshot = null;
/** Lock type pinned by the active record at the time of backup */
private LockType pinnedLockType = null;
/**
* Default c'tor - is a no-op.
*/
protected LightweightUndoable()
{
// nothing to do
}
/**
* Create a new instance and mark it as global depending on the associated flag.
*
* @param global
* Flag identifying if this buffer is associated with a global temp-table.
*/
protected LightweightUndoable(boolean global)
{
setGlobal(global);
}
/**
* Store a reference to the record currently active in the record
* buffer in a new instance of this class and return it.
*
* @return An instance of this class which contains the undoable state
* of the record buffer at the moment it is called.
*/
public Undoable deepCopy()
{
LightweightUndoable copy = new LightweightUndoable();
copy.activeRecord = currentRecord;
copy.activeSnapshot = snapshot;
copy.pinnedLockType = (currentRecord != null
? getPinnedLockType(currentRecord.primaryKey())
: LockType.NONE);
return copy;
}
/**
* Restore to the record buffer the current record and current
* reversible which were set at the time the record buffer was backed
* up for undo support.
*
* @param from
* An instance of this class containing the backup state to be
* restored.
*/
public void assign(Undoable from)
{
if (offEndRollback)
{
return;
}
if (!isActive())
{
// If the buffer is not yet active, then the state will not have
// changed, and there is nothing to do.
return;
}
LightweightUndoable copy = (LightweightUndoable) from;
// reset current record, snapshot, and pinned lock type to what they were before backup
Record active = copy.activeRecord;
setCurrentRecord(active, true, false, false, false);
snapshot = copy.activeSnapshot;
LockType lockType = copy.pinnedLockType;
if (active != null)
{
Long id = active.primaryKey();
LockType pinned = getPinnedLockType(id);
if (lockType == null)
{
lockType = pinned;
}
if (!lockType.equals(pinned))
{
setPinnedLockType(id, lockType);
}
}
}
/**
* Check if this undoable has changed; if so, and we are in a transaction block (and not
* {@code rollingBack}), then register this instance with all blocks up the stack, which
* were started after the object's last save transaction level.
*
* @param changed
* Flag indicating if this undoable has changed or not.
*/
@Override
public void checkUndoable(boolean changed)
{
super.checkUndoable(changed);
}
/**
* Check if this instance is undoable.
*
* @return The same state as returned by {@link RecordBuffer#isUndoable()}.
*/
@Override
protected boolean isUndoable()
{
return RecordBuffer.this.isUndoable();
}
}
/**
* Getter for the {@link #setterDatums} map.
*
* @return See above.
*/
protected Map<String, DatumAccess> getSetterDatums()
{
if (setterDatums == null)
{
setterDatums = new HashMap<>();
}
return setterDatums;
}
/**
* Getter for the {@link #getterDatums} map.
*
* @return See above.
*/
protected Map<String, DatumAccess> getGetterDatums()
{
if (getterDatums == null)
{
getterDatums = new HashMap<>();
}
return getterDatums;
}
/**
* Information needed by a getter or setter method to affect the datum of one property (or one
* element within an extent property), namely, the property's name and optional extent index,
* if accessing a single element in an extent property.
*/
protected static class DatumAccess
{
/** Property name */
private final String propertyName;
/** Index of an element in an extent property or null */
private final Integer extentIndex;
/** The cached accessor for this instance. */
private final Method accessor;
/** The extent value of this property or null. */
private final Integer extent;
/** The cached property meta. */
private final PropertyMeta propertyMeta;
/** Flag indicating if there is an index using this property. */
private final boolean indexed;
/** Flag indicating if there is an ASSIGN trigger for this property. */
private final boolean trigger;
/**
*
* Constructor.
*
* @param propertyName
* Property name.
* @param extentIndex
* Index of an element in an extent property or {@code null}. If present, the index should
* be zero-based.
* @param accessor
* The cached accessor for this instance.
* @param extent
* The extent value of this property or {@code null}.
* @param propertyMeta
* The cached property meta.
* @param indexed
* Flag indicating if there is an index using this property.
* @param trigger
* Flag indicating if there is an ASSIGN trigger for this property.
*/
protected DatumAccess(String propertyName,
Integer extentIndex,
Method accessor,
Integer extent,
PropertyMeta propertyMeta,
boolean indexed,
boolean trigger)
{
this.propertyName = propertyName;
this.extentIndex = extentIndex;
this.accessor = accessor;
this.extent = extent;
this.propertyMeta = propertyMeta;
this.indexed = indexed;
this.trigger = trigger;
}
/**
* Check if this access can be done directly via the field's datum.
*
* @param other
* In case a getter and a setter are used in pair, the 'other' and this property must be of
* the same type.
*
* @return {@code true} if the access can be done directly.
*/
public boolean isDirectAccess(DatumAccess other)
{
// direct access can be used only if the two property have the same type and the dst property is not:
// - mandatory
// - part of an index
// - ASSIGN trigger
return (other == null || propertyMeta.getDataHandler() == other.propertyMeta.getDataHandler()) &&
!(indexed || propertyMeta.isMandatory() || trigger);
}
/**
* Override hash code algorithm
*
* @return Hash code.
*/
public int hashCode()
{
int result = propertyName.hashCode();
result = 31 * result + (extentIndex != null ? extentIndex.hashCode() : 0);
return result;
}
/**
* Override equality algorithm to be consistent with hash code algorithm.
*
* @param o
* Object to be compared.
*
* @return <code>true</code> if this instance is equivalent with the given instance.
*/
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
DatumAccess that = (DatumAccess) o;
if ((this.extentIndex == null) != (that.extentIndex == null))
{
// both must have or both must not have extent
return false;
}
if (this.extentIndex != null && this.extentIndex != that.extentIndex)
{
// if both have but it is different
return false;
}
return propertyName.equals(that.propertyName);
}
/**
* Get the {@link #propertyMeta}.
*
* @return See above.
*/
public PropertyMeta getPropertyMeta()
{
return propertyMeta;
}
/**
* Get the {@link #extent}.
*
* @return See above.
*/
public Integer getExtent()
{
return extent;
}
/**
* Get the {@link #accessor}.
*
* @return See above.
*/
protected Method getAccessor()
{
return accessor;
}
/**
* Get field name.
*
* @return Property name
*/
protected String getPropertyName()
{
return propertyName;
}
/**
* Get extent index (zero-based).
*
* @return Extent index or <code>null</code> if all elements are needed or not an extent
* field.
*/
protected Integer getExtentIndex()
{
return extentIndex;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder("DatumAccess{");
sb.append(propertyName).append("/").append(propertyMeta.getLegacyName());
if (extent != null && extent > 0)
{
sb.append("[").append(extent).append("]");
}
sb.append("}");
return sb.toString();
}
}
/**
* Used as a handler for buffers which are referenced through a buffer parameter. This should
* handle specific DMO alias operations, while any other method should be delegated to the
* back of the proxy - the buffer. The motivation behind this is related to the fact that
* buffer parameters should have the same backing buffer, but still differ through their
* DMO alias and name. This hander will manage both {@link Buffer} proxies and
* {@link RecordBuffer} proxies.
*/
private static class ArgumentBuffer
implements InvocationHandler
{
/** The backing buffer of this proxy, which should handle the majority of the methods */
private BufferImpl buffer;
/** The definition buffer of the buffer instance. */
private RecordBuffer defBuffer;
/** The bound buffer of the buffer instance. Can be the same as {@code defBuffer}. */
private RecordBuffer boundBuffer;
/** The proxy for the definition buffer */
private RecordBuffer defProxy;
/**
* The proxy for the bound buffer. It is the same as {@code defProxy}, if the definition
* buffer is the same as the bound buffer.
*/
private RecordBuffer boundProxy;
/** The DMO alias which should be adopted by the backing record buffer */
private String dmoAlias;
/** The buffer name which should be adopted by the buffer */
private String bufferName;
/**
* The basic initializing constructor.
*
* @param buffer
* The buffer which is going to handle most of the methods
* @param dmoAlias
* The DMO alias which should be adopted by the RecordBuffer
* @param bufferName
* The name which should be adopted by the BufferImpl
*/
ArgumentBuffer(BufferImpl buffer, String dmoAlias, String bufferName)
{
this.buffer = buffer;
this.dmoAlias = dmoAlias;
this.bufferName = bufferName;
this.boundBuffer = buffer.buffer();
this.defBuffer = ((BufferReference) buffer).definition();
this.boundProxy = createProxy();
if (defBuffer == boundBuffer)
{
this.defProxy = this.boundProxy;
}
else
{
this.defProxy = createProxy();
}
}
/**
* Delivers a new proxy backed by this handler. The instance created is made with the
* implicit constructor, so invoking methods on this instance without an invocation handler
* in between, can trigger inconsistent data or exceptions.
* @return A proxy for the specified buffer.
*/
private RecordBuffer createProxy()
{
try
{
@SuppressWarnings("unchecked")
RecordBuffer dmoProxy =
(RecordBuffer) ProxyFactory.getProxy(RecordBuffer.class,
true,
new Class[0],
false,
null,
null,
this);
return dmoProxy;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Proxy handler which should let the methods to the buffer, beside the ones which
* imply the DMO alias or buffer name, which will be handled here.
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
try
{
if (proxy instanceof BufferImpl)
{
if (method.getName().equals("buffer"))
{
return boundProxy;
}
if (method.getName().equals("definition"))
{
return defProxy;
}
// avoid altering methods which should be handled directly by the buffer
// also this is used to suppress the behavior of enclosed argument proxies
if (method.getName().equals("overrideName") ||
method.getName().equals("restoreName"))
{
return Utils.invoke(method, buffer, args);
}
boolean overridden = buffer.overrideName(bufferName);
Object invokeReturn;
try
{
invokeReturn = Utils.invoke(method, buffer, args);
}
finally
{
if (overridden)
{
buffer.restoreName();
}
}
return invokeReturn;
}
if (proxy instanceof RecordBuffer)
{
RecordBuffer backBuffer;
if (proxy == boundProxy)
{
backBuffer = boundBuffer;
}
else
{
backBuffer = defBuffer;
}
// avoid altering methods which should be handled directly by the buffer
// also this is used to suppress the behavior of enclosed argument proxies
if (method.getName().equals("overrideDMOAlias") ||
method.getName().equals("restoreDMOAlias"))
{
return Utils.invoke(method, backBuffer, args);
}
boolean overridden = backBuffer.overrideDMOAlias(dmoAlias);
Object invokeReturn;
try
{
invokeReturn = Utils.invoke(method, backBuffer, args);
}
finally
{
if (overridden)
{
backBuffer.restoreDMOAlias();
}
}
return invokeReturn;
}
}
catch (Throwable t)
{
Throwable cause = t;
while (cause != null && !(cause instanceof ConditionException))
{
cause = cause.getCause();
}
if (cause instanceof ConditionException)
{
throw cause;
}
else
{
throw t;
}
}
// should be unreachable
return null;
}
}
/**
* Obtain a string representation of the table content stored on SQL. This is a SQL-wise view of the
* primary table of the DMO, not an actual ABL data. Only to be used in debug purposes.<br>
* Notes:
* <ul>
* <li>the EXTENT properties are not shown unless the table is denormalized (in which case the these
* properties are stored in the primary table);
* <li>the name of the columns in the returned string are the SQL column names, not the legacy names;
* <li>the hidden, reserved before/after-table specific attributes are visible for temp-tables;
* <li>the larger string/character values are cut to max 32 chars;
* </ul>
*
* @param limit
* The maximum number of rows to be retrieved. Use 0 or negative to get them all.
*
* @return A string with the table content in a tabular format.
*
* @throws PersistenceException
* @throws SQLException
* If any issue was encountered. Since this method is supposed to be called only in debug mode,
* it's up to programmer to decide what went wrong.
*/
public String sqlTableContent(int limit)
throws PersistenceException, SQLException
{
String sql = "select * from " + dmoInfo.sqlTable;
if (limit > 0)
{
sql += " limit " + limit;
}
Connection conn = persistence.getSession(!dmoInfo.multiTenant).getConnection();
try (PreparedStatement ps = conn.prepareStatement(sql))
{
ResultSet res = ps.executeQuery();
ResultSetMetaData md = res.getMetaData();
int cc = md.getColumnCount();
int[] lens = new int[cc];
List<String[]> rows = new ArrayList<>(10);
while (res.next())
{
String[] row = new String[cc];
for (int k = 1; k <= cc; k++)
{
row[k - 1] = res.getString(k);
int length = row[k - 1] == null ? 4 : row[k - 1].length();
if (lens[k - 1] < length)
{
lens[k - 1] = length;
}
}
rows.add(row);
}
StringBuilder sb = new StringBuilder();
sb.append("ABL: ").append(dmoInfo.legacyTable).append("/ SQL:").append(dmoInfo.sqlTable).append("\n");
for (int k = 1; k <= cc; k++)
{
String label = md.getColumnLabel(k);
if (lens[k - 1] < label.length())
{
lens[k - 1] = label.length();
}
if (lens[k - 1] > 32)
{
lens[k - 1] = 32;
}
else if (lens[k - 1] < 4)
{
lens[k - 1] = 4;
}
if (k != 1)
{
sb.append(" | ");
}
fit(sb, label, lens[k - 1], md.getColumnType(k));
}
sb.append("\n");
for (int k = 1; k <= cc; k++)
{
if (k != 1)
{
sb.append("-+-");
}
int len = lens[k - 1];
while (len-- > 0)
{
sb.append("-");
}
}
sb.append("\n");
for (String[] row : rows)
{
for (int k = 1; k <= cc; k++)
{
if (k != 1)
{
sb.append(" | ");
}
fit(sb, row[k - 1], lens[k - 1], md.getColumnType(k));
}
sb.append("\n");
}
res.close();
return sb.toString();
}
}
/**
* Helper method for {@link #sqlTableContent(int)}. Used for generating the tabular pretty format.
*
* @param sb {@code StringBuilder} to put the result into.
* The
* @param text
* The value to be formatted.
* @param size
* The available space.
* @param type
* The column type. Used for alignment.
*/
private void fit(StringBuilder sb, String text, int size, int type)
{
boolean leftAlign = false;
switch (type)
{
case Types.NCHAR:
case Types.NVARCHAR:
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
leftAlign = true;
break;
}
String ret;
if (text == null)
{
ret = "null";
}
else if (text.length() <= size)
{
ret = text;
}
else
{
ret = text.substring(0, size);
}
int k = size - ret.length();
if (leftAlign)
{
sb.append(ret);
}
while (k-- > 0)
{
sb.append(" ");
}
if (!leftAlign)
{
sb.append(ret);
}
}
/**
* Helper class to extract DMO super Interface from Buffer interface.
*
* @param iface
* The buffer interface.
*
* @return DMO type interface
*
*/
public static Class<?> extractDMOInterface(Class<?> iface)
{
if (Buffer.class.isAssignableFrom(iface))
{
// this should have been DMO interface and not Buffer interface
for(Class <?> superIface:iface.getInterfaces())
{
if (DataModelObject.class.isAssignableFrom(superIface))
{
return (Class<?>) superIface;
}
}
}
return iface;
}
/**
* Check if OffEnd is reversible.
* Reversible OffEnd treats OffEnd.FRONT as OffEnd.BACK for PREV/LAST queries
* @return true if OffEnd is reversible
*/
boolean isOffEndReversible()
{
return reversibleOffend;
}
/**
* Set offEnd to OffEnd.FRONT and allow it to be treated as OffEnd.BACK for PREV/LAST queries
*/
void allowReversibleOffEnd()
{
this.offEnd = OffEnd.FRONT;
this.reversibleOffend = true;
}
/**
* Disable OffEnd from being reversible for for PREV/LAST queries
*/
void disableReversibleOffEnd()
{
this.reversibleOffend = false;
}
}