TransactionManager.java
/*
** Module : TransactionManager.java
** Abstract : transaction/block properties helper methods
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
** 001 GES 20050809 @22096 Created initial version, supporting
** transaction/block properties with full
** support for nested blocks, undo and retry.
** 002 GES 20050825 @22242 Added global finish processing.
** 003 ECF 20050920 @22888 Various changes to support database
** transaction processing. Added registration
** and use of ScopeableFactory interface.
** Reversed order of Commitable notifications.
** Delayed notification of opening scope until
** after transaction (if any) is begun.
** 004 GES 20051005 @22999 Added master commit and master finish
** notifications that are guaranteed to occur
** after all other individual commits/finishes.
** 005 ECF 20051007 @23005 More changes to support database transaction
** processing. Changed isTransaction()'s access
** to public. Removed unused, private variant of
** isFullTransaction() and added public, no-arg
** variant of this method.
** 006 GES 20051007 @23007 Added rollback notification for the updated
** Commitable interface AND added master
** rollback support.
** 007 ECF 20051104 @23234 Changes to support iterate notifications.
** Finalizables are now notified at each call to
** the iterate() method.
** 008 ECF 20051104 @23261 Update to honorError() method. Added code to
** report to stderr the error messages embedded
** in an exception chain, in the case where the
** error is not rethrown to a higher block.
** 009 ECF 20051108 @23277 Add deferred commit/rollback error handling.
** This defers throwing an exception thrown by
** a commit/rollback notification callback and
** caught during processCommit/Rollback until it
** is safe to do so. Logging added as well.
** 010 ECF 20051108 @23397 Added isTopLevelBlock() method. Fixed
** popScope(), which was popping its scope too
** early. Use peek instead, then pop at end of
** method.
** 011 ECF 20051202 @23596 Allow multiple master Commitables and
** Finalizables to be registered. Also extended
** deferred error handling to Finalizable
** notification processing.
** 012 GES 20060123 @24019 Moved to context-local storage and fixed a
** couple of deferredError bugs.
** 013 GES 20060128 @24114 Added isGlobalBlock().
** 014 GES 20060130 @24142 Added processing quit state flag for global
** exiting.
** 015 ECF 20060130 @24225 Added registerTopLevelFinalizable() method.
** Also added logging to report push/pop scope
** events.
** 016 GES 20060202 @24228 Added support for differentiating external
** blocks (external procs) from other top-level
** blocks (any proc/func/trigger).
** 017 GES 20060203 @24245 Changed method name makeBackup() to
** blockSetup(). This is done because this
** hook now has responsibilities to handle
** iterate() to compensate for usage of explicit
** continue (which would bypass the iterate()
** call).
** 018 GES 20060204 @24278 Added batch notification support.
** 019 GES 20060207 @24329 Check interrupted status in strategic hooks
** and throw a StopConditionException in the
** case where the thread has its interrupted
** status set.
** 020 ECF 20060206 @24344 Prevent infinite recursion loop in
** ContextContainer.obtain. Was caused when
** ScopeableFactory.createScopeable called into
** TransactionManager while ContextContainer was
** still initializing WorkArea.
** 021 GES 20060302 @24856 Added removeFinalizable() to facilitate
** cleanup of resources before finished().
** 022 ECF 20060309 @24997 Modified auto-remove processing for master
** commitables and finalizables. Auto-remove
** processing was too aggressive; it must occur
** only for popScope for a full transaction.
** 023 GES 20060314 @25060 Moved to common logging infrastructure and
** added major debug logging.
** 024 GES 20060322 @25193 New rollback() signature.
** 025 ECF 20060502 @25947 Added registerNearestLoopFinalizable()
** method. Finds nearest enclosing block which
** defines a loop and registers a Finalizable to
** it.
** 026 GES 20060510 @26056 New state to externally query if an iteration
** is being called from the top or the bottom of
** the block.
** 027 GES 20060517 @26191 Added retry notification processing.
** 028 GES 20060518 @26274 Use an identity based map for undoables to
** solve problem where two keys with the same
** value can't be separately stored in the map.
** 029 GES 20060529 @26649 Iterate processing must trip processing
** rollback if there is a pending rollback
** since we may have gotten here via a NEXT
** and otherwise the rollback may not occur.
** Deferred rollback is also triggered before
** getting to the labeled block in the case
** where the block associated with the full
** transaction is enclosed (more deeply nested)
** in the labeled block. The rollback is then
** triggered at the full transaction level.
** 030 GES 20060608 @27024 Added a method to abort further processing
** in a block in triggering a RETRY as part of
** an UNDO statement (as opposed to processing
** a RETRY from within a catch block).
** 031 GES 20060609 @27055 Converted deferred error processing to catch
** and process Exception rather than the more
** specific ErrorConditionException. This is
** required whenever we have some unexpected
** exception (an abnormal end). Otherwise the
** deferred error handling did not trigger and
** the state of the TM is corrupted. This had
** bad effects as the finally blocks called
** popScope() up the stack.
** 032 ECF 20060610 @27089 Add processing of master transaction retries.
** Finalizables registered for master
** transaction processing were not receiving
** retry notifications.
** 033 GES 20060626 @27565 Support silent error mode in the calling
** method from triggerErrorInCaller().
** 034 GES 20060804 @28426 Modified block processing to ensure that
** the Finalizable.iterate() notifications only
** occur from the top of the block.
** 035 GES 20060809 @28514 Added a new concept of a lightweight block
** which allows the differentiation between
** a Progress DO block and a REPEAT/FOR block
** (or really any other block types in
** Progress). There are new transaction level
** constants and helpers to test and query
** these levels.
** 036 GES 20060908 @29363 Retry must honor pending rollbacks for the
** current block.
** 037 GES 20060921 @29832 Fix to force a default retry at the same
** level as any pending rollbacks if the block
** is not specified (if it is null). Previously
** only the current block was retried even when
** an enclosing block was the level of the
** rollback. Also fixed the fact that loop
** protection disabling must propagate up to
** enclosing blocks during popScope.
** 038 ECF 20061002 @30069 Fix to rollback(). Only defer rollback if
** current scope is not a subtransaction or a
** transaction. Also prevent unnecessary retry
** processing from popScope().
** 039 GES 20061004 @30133 Support for nested errors (an error generated
** during processing of another error, while
** nested error mode is enabled).
** 040 GES 20061009 @30219 Honor null return from createScopeable() to
** allow specific factories to decide NOT to
** register an object for a given context.
** 041 GES 20061010 @30263 Added registerNextExternal() and deferred
** external procedure block registration for
** use by objects being constructed in an
** initializer/constructor but needing to be
** registered in the next external proc's block.
** 042 ECF 20061011 @30354 Fixed methods processFinalizables() and
** processRetry(). Master finalizables were not
** being processed in the case where no
** non-master finalizables were registered in
** the same scope.
** 043 GES 20061116 @31215 Converted all internal processing from using
** block labels to identify blocks, into using
** an index into the stack of blocks. This is
** a unique value whereas the label can be
** duplicated in nested blocks. In particular,
** there was a case where the full transaction
** flag was being cleared because a nested
** block had the same label as the one that was
** marked as the full transaction.
** 044 ECF 20061116 @31240 Fixed sprintf spec in WorkArea. Was using %s
** where %i is required, raising an exception in
** the Format class.
** 045 GES 20070409 @32860 When deferred errors have a condition as a
** cause, unpack that cause and throw that
** instead of the wrapper. This negates the
** effect of runtime components that are not
** Progress aware, which wrap normal conditions
** in more generic exceptions. Since the 4GL
** compatible runtime knows how to deal with
** conditions, the behavior will be correct.
** 046 NVS 20070515 @33490 Fixed ArrayIndexOutOfBoundsException in the
** toString() method.
** 047 GES 20071213 @36341 Disallow multiple finalizable registrations
** for the same object in the same block.
** 048 GES 20071213 @36352 Added deferError support to scope
** notification processing.
** 049 ECF 20071214 @36377 Added protection for iteration of certain
** WorkArea resources. To prevent a concurrent
** modification exception, updates to the work
** area's scopeList, deferredFinalizables,
** masterCommit, and masterFinish collections,
** and to the current block's finalizables and
** commitables collections, are disallowed while
** the respective resource is being iterated.
** A warning is logged with a stack trace. This
** prevents catastrophic failure, but also
** signals a programming error that should be
** fixed.
** 050 GES 20071218 @36428 Exposed the current finalizable operation
** in process.
** 051 GES 20071218 @36439 Added abnormalEnd() to notify the TM about
** any unexpected failure, allowing any database
** state to be rolled back (keeping integrity
** of the database).
** 052 CA 20080207 @37027 Added constants for different types of blocks.
** Added a new pushScope(...) method which
** receives the type of block being entered -
** only editing blocks are currently supported.
** Added a method to retrieve the type of block
** which is currently active.
** 053 ECF 20080218 @37118 Fixed processRollback(). Rollback processing
** must occur before undo processing. Otherwise,
** we might be performing rollback processing on
** the wrong records in RecordBuffer.
** 054 ECF 20080221 @37142 Added protection code to WorkArea.toString().
** Also rolled back #052 (@37118), which caused
** regressions.
** 055 ECF 20080430 @38182 Fixed processRollback(). Reinstated #053
** (@37118). SVL's related fix to RecordBuffer
** corrects the regressions reported in #054
** (@37142).
** 056 GES 20080505 @38210 Moved undoables to be an array for better
** performance.
** 057 GES 20080612 @38756 Uncaught condition exceptions should UNDO
** the full transaction level as they pass
** through, unless the condition is QUIT (which
** has the behavior of committing).
** 058 GES 20080619 @38888 Massive update of infinite loop protection
** to fix deviations from Progress behavior.
** This includes ILP for NEXT (honorNext()),
** PAUSE support for sometimes disabling ILP
** (depending on runtime context), special
** behavior for RETRY caused by an ENDKEY,
** a "stutter" mode for RETRY conversion et al.
** Fixed some "holes" in exception processing
** that would allow an UNDO, RETRY cause inside
** a transaction but which is targeted at a
** block that is outside the transaction, to
** avoid rolling back at the full transaction
** level. Added the ability to deregister a
** list of undoables for the current scope (and
** nested scopes). This is needed to allow
** no-undo parameters to be properly supported.
** 059 GES 20080624 @38916 Limiting registration of undoables to a
** single instance is being removed because
** it forces a compareTo which is potentially
** dangerous since some undoables may have
** errors when compared to each other (in
** unexpected ways). It is also expensive so
** it is good to avoid it for that reason.
** 060 GES 20080625 @38960 Moved to the BlockType enum.
** 061 GES 20080627 @38992 Fix for honorNext().
** 062 ECF 20080630 @39031 Added registerNext(). Defers registration of
** an Undoable until the next block scope opens.
** 063 CA 20080714 @39102 Fixed deregister() memory leak: the
** backupUndoList was keeping the state of the
** undoables after the entry of the current
** block, not before.
** 064 GES 20080811 @39381 New features for UNDO and RETRY processing.
** 065 CA 20080826 @39565 Added processRollbackPending - notify the
** commitables that a rollback is pending to be
** executed by a parent block.
** 066 CA 20080815 @39448 Added honorStop(). This method will decide if
** the processing of a STOP condition will be
** vetoed at the current scope.
** 067 GES 20080818 @39858 Added some register() signatures to provide
** simpler defaults. Removed the commit feature
** of register() since it wasn't used anywhere.
** 068 GES 20080930 @39976 Reworked undo registration to allow a cleaner
** interface to the caller. Now it allows
** varargs to minimize the number of calls.
** 069 GES 20081003 @40036 Added _isRetry().
** 070 SIY 20081002 @40015 Inferred generics. Added helper method
** notifyCondition().
** 071 SVL 20081024 @40219 Added findNearestExternal() function.
** 072 SIY 20081111 @40411 Call notifyCondition() if real problem is
** detected in abnormalEnd().
** 073 CA 20081127 @40708 Added 'block iterated' support - method
** hasParentIteratedBlock will check if any
** of the current blocks iterated at least
** once.
** 074 GES 20081126 @40742 Added helpers to access the properties of
** a block and to manage the number of
** interactions the current block has had.
** 075 GES 20081210 @40866 Added support for controlling the rollup
** flag for the current block. Honor the rollup
** flag for the containing block when exiting
** the current block.
** 076 GES 20090115 @41131 Only wrap abend exceptions once. Modified
** abnormalEnd() signature to avoid duplicate
** notifications. In places where we re-throw
** a condition exception, we now ensure that
** abnormalEnd() is used to allow the rollback
** processing to happen.
** 077 GES 20090422 @41917 Converted to standard string formatting.
** 078 ECF 20090714 @43189 Reworked deferred error processing. Deferred
** error is now a Throwable. Fixed abnormal end
** processing such that an exception thrown during
** notification or rollback does not mask the root
** cause of the abnormal end.
** 079 GES 20090729 @43442 Added processValidate() method to call validate()
** on registered Commitable instances.
** 080 CA 20090731 @43472 Added code for off-end listener registration.
** When a FOR or FOR EACH block is initialized, all
** its associated queries must have their off-end
** listeners registered, to allow block termination
** when the query goes off-end.
** 081 CA 20090804 @43506 Added getOffEndListeners - returns the topmost
** set of query off-end listeners and eventually
** clears that set.
** 082 ECF 20090816 @43655 Added methods to register and deregister an
** OutputParameterAssigner to/from the current
** block.
** 083 CA 20090821 @43716 When deferring a STOP condition caused by thread
** interruption, it will not be logged as SEVERE
** (to avoid bogus stacktraces in server log).
** 084 ECF 20090826 @43785 Rolled back #008 (@23261). It is unclear why this
** was added and it outputs directly to stderr,
** seemingly unnecessarily.
** 085 GES 20090831 @43804 Save and restore error mgr silent mode flag
** across top-level block boundaries. Removed
** previous stacking support for the error mgr,
** which was over-stacking things.
** 086 GES 20090915 @43898 At the end of retry processing, honor deferred
** errors and any pending stop condition.
** 087 CA 20090917 @43931 Fix abnormal connection end. If the TM was
** notified to abort processing, the next
** honorStopCondition call will throw a
** SilentUnwindException. So, no one can interfere
** with the shutdown.
** 088 CA 20090922 @43979 In deferError, log SilentUnwindException's using
** FINER instead of SEVERE level.
** 089 GES 20090924 @44011 Added a context-specific headless flag.
** 090 SVL 20091117 @44398 Added support for the Resettable objects.
** 091 CA 20091106 @44320 Protect the rollback processing against any
** thread interruption incoming from a CTRL-C or a
** client abend, by marking it as a critical
** section.
** 092 CA 20091202 @44470 Added methods for retrieving the current nesting
** level and the level of the block which is started
** a full transaction.
** 093 GES 20100330 @44786 Added setHeadless() to allow per-context control
** over headless mode. This can also be done via the
** directory, but this interface allows a dynamic
** approach.
** 094 ECF 20121114 Fix for Java 7: modified pushScope to deal with
** varargs in overloaded register method.
** 095 ECF 20130121 Fix for Java 7: Added registerGlobal to
** disambiguate from overloaded register methods.
** 096 CA 20130126 Save the Block instance in a block definition
** field (BlockDefinition.block).
** Added nearestExecutingBlock.
** 097 CA 20130209 Added dynamic EXTENT and EXTENT parameter support.
** 098 SVL 20131028 Added registerFinalizableAt, registerCommitAt, isTransactionAt.
** 099 CA 20131206 Added delayed processing of code which normally would be executed
** by a finalize or scopeFinished implementation. This processing will
** call deleted() or scopeDeleted() implementations, immediately if the
** current THIS-PROCEDURE is not persistent or when the procedure gets
** explicitly deleted, otherwise.
** 100 SVL 20140106 Added "silent" parameter to deferError function, changed function
** access modifier.
** 101 CA 20140109 Do not cache context-local vars, as they may get reset (in case of a
** state-reset appserver Agent).
** 102 CA 20140224 Finalizable and scope processing needs to be aware of persistent
** procedures marked for "delete-on-exit".
** 103 SVL 20140221 findNearestTopLevel was made public.
** 104 CA 20140301 Procedure's scope is notified first for start and last for finish.
** 105 SVL 20140414 Modified isGlobalBlock in order to support agent contexts.
** 106 CA 20140513 Added a weight for the context-local var, to ensure predetermined
** order during context reset.
** 107 ECF 20140813 Changed access of processValidate method to package private.
** 108 SVL 20150115 Added _isHeadless which gets "headless" attribute without initiating
** any contexts in TM, LT etc.
** 109 SVL 20150116 Fixed _isHeadless for conversion case.
** 110 OM 20150506 Added public method nearestTopLevelType(). Upgraded code to Java7.
** 111 ECF 20150510 Added deregisterGlobalFinalizable to enable explicit removal of a
** finalizable from the global list.
** 112 ECF 20150801 Added support for compound query optimization.
** 113 OM 20160126 Do not create unneeded context local on abortProcessing(). Server
** context is automatically headless. isHeadless() made public.
** ECF 20160128 Minor performance improvement: avoid context-local lookup when
** possible in isGlobalBlock.
** 114 ECF 20160206 Store the exception, if any, which triggered a rollback, and log that
** exception and the current one, if available, when we detect the
** illegal state of triggering a rollback when one already is pending.
** Changed exception type from UnsupportedOperationException to
** IllegalStateException in this case.
** 115 OM 20160223 Added cleanupDeferredFinalizables() method.
** 116 IAS 20160408 Use synthetic key as context key.
** 117 ECF 20160517 Prevent duplicate Undoable instances from being added to the global
** scope.
** 118 GES 20160523 Minor code cleanup.
** 119 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.
** Refactored context-local resolution in the TM APIs, so that the
** WorkArea instance is resolved only once and then re-used.
** CA 20160721 Reworked variable/parameter definitions and initialization to use
** UndoableFactory and TypeFactory APIs (thus TM.register for undoables
** is no longer required in converted code).
** CA 20160728 Fixed undo support for dynamic extent vars. Fixed undo support for
** variables defined in external programs ran persistent.
** 120 CA 20160812 Performance improvements for H119: use IdentityHashMap when using
** ext prog instances as keys, pass the tx level when registering
** dynamic extent arrays.
** 121 ECF 20160827 Additional refactoring to avoid resolving WorkArea more often than
** necessary.
** 122 CA 20170427 _isHeadless() will always return true for virtual sessions.
** 123 CA 20171030 Added isSuppressError.
** ECF 20171030 Implemented transaction metadata.
** 124 ECF 20180201 Removed query optimization call from stopOffEndRegistration.
** 125 GES 20180702 Do not propagate ILP and interactions state past a top level block.
** 126 CA 20181112 Added support for the TRANSACTION resource.
** 127 CA 20190219 Added getBlock(levels).
** GES 20190324 Refactored to create a TransactionHelper inner class which contains
** instance method versions of the external static TM interface. That
** legacy API has its implementation moved into private static workers
** which take a WorkArea as the first parameter. There is an accessor
** to get a new instance of the TransactionHelper which contains an
** instance of the WorkArea. This allows all instance method API usage
** in TransactionHelper to directly use the private static workers
** without any context local lookup. Since the original API is intact,
** code can be moved to this new approach over time.
** CA 20190325 Implemented ProcedureHelper instead of direct usage of the static
** API. This allows the elimination of context local usage in
** ProcedureManager.
** 128 ECF 20190714 Added support for aggressive buffer flushing mode.
** 129 CA 20190710 Made deregisterOutputParameterAssigner public.
** CA 20190722 Changed offEndQueries type to HashSet.
** HC 20190724 Changed abend log level.
** CA 20190728 Sort the finalizables by their weight, ascending. Required by the
** output table/dataset cases, which needs to be performed before the
** buffer's scope is closed.
** OM 20190731 Overloaded triggerErrorInCaller to accept numeric error code.
** 130 AIL 20191119 Added more scenarios in which an error should be honored. Made aware
** of DATABASE TRIGGER DataType.
** 131 CA 20200312 Fixes for UNDO, THROW and RETURN ERROR, in case when a OO-like
** error condition needs to be generated.
** CA 20200324 More fixes related to OO-like error processing via AppError.
** 132 GES 20200316 Added invocation of Finalizable.entry() in blockSetup() (only the
** first time it is executed for a block or loop).
** ECF 20200419 Change to BufferManager.endTxPost method signature.
** 133 AIL 20200518 Added utilities for working with blocks: getBlockAtDepth and countBlocks.
** AIL 20200616 Added more operations on the block stack.
** AIL 20200622 Removed getStack method.
** Updated copyStack method.
** GES 20200730 Deleted setHeadless() which was dead code. Eliminated ErrorManager context-
** local lookups.
** CA 20200927 Avoid context-local lookups by relying on the helper instance.
** CA 20201003 Use an identity HashSet where possible.
** CA 20201015 Replaced java.util.Stack with a non-synchronized custom implementation.
** OM 20201125 Javadoc update.
** CA 20210310 Small performance improvement related to offend query/listener processing.
** ECF 20210504 Adjusted for API changes in BufferManager.
** CA 20210629 Database triggers may get invoked while processing finalizable for a external
** program delete, so the processing procedure must be saved/restored and not set
** to null.
** GES 20210809 Moved FINALLY block execution to occur after commitables/finalizables in
** iterateWorker(), processRetry() or popScope(). If a finally block must be
** processed, it must be explicitly enabled during each block iteration and
** after execution here, it is cleared. If the finally block is not enabled
** using TransactionHelper.setupFinallyBlock(), then no finally processing will
** occur during that iteration. During execution of the finally block (in
** processFinallyBlock(), at the full transaction level we clear the transaction
** level and nested transaction level state and then restore it after the finally
** block executes. This duplicates the behavior where in the 4GL, the finally
** block does not report it is in a transaction when executed at the full
** transaction level.
** CA 20210906 'processFinallyBlock' must be enclosed in a try/finally block, so any conditions
** raised from it will ensure the parent block is processed correctly.
** OM 20211111 Added transaction support for finally blocks.
** OM 20211201 Improved transaction support for finally blocks.
** OM 20211203 Ignore ReturnUnwindException when processing the finally block of a proc/func.
** CA 20211222 Fixed memory leak: when a procedure gets deleted, all its undoables must be
** deregistered; stale procedures can be registered with this block, when its FINALLY
** code is executed, so ensure DeferredDeletablesManager gets executed after the
** FINALLY code completes, as, the block's finalizables are already processed before
** FINALLY executes.
** ECF 20220103 Do not propagate aggressive FOR flushing to nested blocks.
** CA 20220114 Added SESSION:CLIENT-DISCONNECTED FWD extension attribute.
** Added SESSION:TERMINATION-HOOK, a hook to be executed when the global block is
** being terminated, to allow application-specific cleanup. The cleanup code must
** not perform any client-side calls, for i.e. file, socket access, UI, etc; any
** such code can be avoided via the CLIENT-DISCONNECTED flag.
** CA 20220120 Improved performance for finalizables, keep them already sorted by their weight.
** ConditionListener's are kept in their own list.
** CA 20220128 TERMINATION-HOOK can not be set for remote sessions (appserver).
** CA 20220210 Removed Commitable.rollbackPending and its notifications, as there is no actual
** implementation of this method.
** CA 20220310 Backed out a change in CA/20220210 - in popScope(), the global scope is finalized
** only when the root block is popped (this includes appserver agents).
** RFB 20220712 Added isRootExternal method which is similar to isRootNestingLevel, but it uses
** the nearest external block nesting instead of the root. Ref #6578.
** 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 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 process commit, rollback and validate. Refs #6814
** CA 20221006 Lazy initialization for commitables and conditionListeners. 'finalizables' can be
** a set instead of a list, as their order is not important. Refs #6824
** CA 20221010 Performance improvements - avoid the ProcedureData lookup, by keeping a parallel
** stack of this data for THIS-PROCEDURE. Refs #6826
** CA 20220602 TRANSACTION-MODE AUTO is opened at the agent's call scope, not the root scope.
** CA 20220630 Fixed getBlockAtDepth - if 'blockDepth' is 0, return global block.
** CA 20220918 Performance improvement - do not save procUndoables if there are no undoables at
** that external program.
** CA 20221010 Performance improvements - avoid the ProcedureData lookup, by keeping a parallel
** stack of this data for THIS-PROCEDURE. Refs #6826
** SVL 20230108 Improved performance by replacing some "for-each" loops with indexed "for" loops.
** CA 20230110 A small improvement for 'addFinalizable'.
** CA 20230116 Having a package-private inner-class field accessed from another inner-class
** avoids a synthetic getter method emitted by javac.
** HC 20230118 Eliminated some of the uses of String.toUpperCase and/or
** String.toLowerCase for performance.
** 134 CA 20230223 Avoid listIterator usage, as a plain for loop can be used, in 'nearestTopLevel'.
** 135 SVL 20230317 offEndQueries is now a stack: fixed the set of off-end queries being wrong when
** a database trigger is invoked because of buffer flush in FOR cycle init block.
** 136 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 137 CA 20230615 The CATCH block must be executed just before the FINALLY block, in popScope.
** Refactored masterFinish/masterCommit to default to null, and also moved the
** management of this for CATCH and FINALLY in a common block (for popScope).
** 138 CA 20230607 TRANSACTION-MODE AUTO must preserve the tx in the root appserver block - this is
** required because in modes other than State-free, requests can be executed on the
** same agent who had TRANSACTION-MODE AUTO, and this must see as active the tx
** initially set.
** 139 GBB 20230616 Remove log for exceptions caused by ApplicationRequestedStop.
** 140 CA 20230713 Fixed a regression in H137 - a RETURN from a CATCH block was leaking to previous
** block, and terminating it early.
** Small optimization, use TM.getBlock() which returns the top of the stack, instead
** of TM.getBlock(0).
** 141 CA 20230712 Fixed 'isRootExternal' when the target on the appserver is an internal entry for
** a persistent program.
** 142 GBB 20230825 SecurityManager context & session methods calls updated.
** 143 CA 20231026 Do not pin the Block instance to BlockDefinition, as it is never read.
** CA 20231031 Added 'scopeStart(BlockDefinition)', required when a top-level scopeable is lazy
** registered while in an inner block.
** 144 CA 20231214 Fixed a 'pushScope' API which was passing UNKNOWN as the block type instead of
** the parameter.
** 145 CA 20231216 Avoid iterators when processing BlockDefinition.finalizables.
** CA 20231221 Refactored the WidgetPool scopeable support to register for processing as needed.
** 146 HC 20240222 Enabled JMX on FWD Client.
** 147 CA 20240320 Avoid BDTs within internal FWD runtime. Replaced iterators with Java 'for',
** where it applies.
** 148 AL2 20240322 Fixed typo when deregistering undoables from all tx blocks.
** 149 DDF 20240404 Reverted change made when deregistering undoables.
** 150 SP 20240422 Added processUndoables(boolean currentBlock).
** 151 DDF 20240509 Call deleted() when deregistering a Finalizable from masterFinish collection,
** but only for a transaction initiated by TRANSACTION-MODE AUTO.
** DDF 20240509 Replace usage of autoRemove() with autoRemoveAndDelete().
** 152 SB 20240523 Added attemptRollback. Refs #8728.
** 153 CA 20240609 Added new overloads of 'triggerErrorInCaller'.
** 154 SVL 20240621 Pass error code to the client side in triggerErrorInCaller.
** 155 CA 20240707 If there is a pending error when executing a FINALLY block, execute this in its
** own 'nestedSilent' mode, as a FINALLY block can not override errors from the main
** block.
** 156 ES 20240711 Call cleanupPending in case of StopConditionExeption is thrown by a stop-after timer.
** 157 CA 20240809 Avoid iterator usage, for performance improvement.
** 158 SP 20240801 Extracted processCommit, processRollback and processValidate lambdas
** to processCommitImpl, processRollbackImpl and processValidateImpl methods.
** Skip using JMX timers when JMX_DEBUG flag is not set.
** 159 AS 20240924 Use triggeredInCaller attribute when creating an
** ErrorConditionException in triggerErrorInCaller().
** 160 TJD 20230410 Check if rollback is already done with hadRollback function
** TJD 20230824 Mark full transaction block as rolledBack on deferred rollback to allow proper
** restore of record locks
** 161 ES 20241009 Setting undoThrow flag in case a rollback of a block happend.
** 162 CA 20240911 When resize a dynamic extent field via deserialization, undoables must be
** registered in the deserialized instance, as this instance is not on stack.
** 163 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 164 CA 20250127 If a persistent procedure's delete is delayed, then register the BufferManager
** scopeable, as this will be needed when the current scope ends, to delete the
** procedure's resources.
** 165 GBB 20250403 Added getScopeLabel. BlockManager conditions to be enum constants.
** 166 ES 20250411 Combined execution logic for finally and catch blocks in processTerminationBlock method.
** 167 ES 20240221 Switch to throwing the StopConditionException through ErrorManager.
** ES 20250228 Cleaning up the stopAfterTImer when a Progress.Lang.Stop condition was
** thrown.
** ES 20250228 Register the stopAfterTimer for the WorkArea of the TM.
*/
/*
** 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.util;
import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.oo.lang.Stop;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.ui.LogicalTerminal;
import com.goldencode.util.*;
import com.goldencode.util.Stack;
import static com.goldencode.p2j.util.BlockType.*;
/**
* Progress compatible transaction/block properties static utility methods
* which operate upon a context-local scoped dictionary of blocks. This
* class provides the following features:
* <p>
* <ul>
* <li> Maintains state for each block, handling the proper nesting of
* blocks.
* <li> Tracks the start and end of transactions and nested
* sub-transactions.
* <li> Provides support for sending notifications of block start/end to
* registered instances of the {@link Scopeable} interface.
* <li> Provides a rollback service within a transaction or sub-transaction
* triggered by known error conditions or by explicit request. See
* {@link Undoable} for externally driven rollback and see the
* {@link Commitable} for rollback notifications and custom rollback
* support. See {@link #registerNext}, {@link #registerCurrent} and
* {@link #registerCommit}.
* <li> Provides a commit service triggered at known successful completion
* of a transaction or sub-transaction. See {@link Commitable} and
* {@link #registerCommit} for commit notifications.
* <li> Provides a cleanup service for a callback on any exit from a given
* scope (whether that exit was normal or abnormal). See the
* {@link Finalizable} interface and {@link #registerFinalizable}.
* <li> Provides an iteration service triggered by client code invocation
* of the iterate() method. See the {@link Finalizable} interface
* and {@link #registerFinalizable}.
* <li> Provides a retry callback triggered by client code invocation
* of the needsRetry() method. See the {@link Finalizable} interface
* and {@link #registerFinalizable}.
* <li> Provides a session-level cleanup service which is called after the
* exit of the top-most scope. This global finish processing occurs
* when the last scope is popped from the stack of scopes. For this
* reason, the current implementation assumes that the session is
* over at the moment that the top-most user-defined scope exits.
* See {@link #registerFinalizable} and note the use of the
* <code>global</code> option.
* <li> Maintains state regarding retry logic to allow blocks or loops to
* be retried as a result of error conditions being raised.
* <li> Maintains state and processing of retry and error stack unwinding.
* <li> Provides a session-level batch start and stop service.
* <li> At every block start, stop and iteration the current thread's
* <code>interrupted status</code> is checked and if the thread
* has been previously interrupted, a {@link StopConditionException}
* will be thrown. See {@link #honorStopCondition}.
* </ul>
* <p>
* The following is the mapping of Progress language features to the
* corresponding feature in this class:
* <p>
* <pre>
* Progress Name Type Method
* --------------- -------------- -----------------------------------
* transaction function {@link #isTransactionActive}
* retry function {@link #isRetry}
* </pre>
* <p>
* This class minimizes the amount and complexity of client code by using
* lists of objects to notify of specific events. The following is a
* summary of the event processing:
* <p>
* <pre>
* Interface Registration Method
* ------------ ---------------------------
* Undoable {@link #registerNext} {@link #registerCurrent}
* Commitable {@link #registerCommit}
* Finalizable {@link #registerFinalizable}
* Scopeable {@link #registerScopeable}
* BatchListener {@link #registerBatchListener}
*
* Interface Notify Method(s) Event
* ------------ ------------------- ------------------------
* Undoable {@link #pushScope} {@link Undoable#deepCopy}
* {@link #blockSetup} {@link Undoable#deepCopy}
* {@link #rollback} {@link Undoable#assign}
* {@link #popScope} {@link Undoable#assign}
* Commitable {@link #blockSetup} {@link Commitable#commit}
* {@link #popScope} {@link Commitable#commit}
* {@link #rollback} {@link Commitable#rollback}
* {@link #popScope} {@link Commitable#rollback}
* Finalizable {@link #popScope} {@link Finalizable#finished}
* {@link #blockSetup} {@link Finalizable#iterate}
* Scopeable {@link #pushScope} {@link Scopeable#scopeStart}
* {@link #popScope} {@link Scopeable#scopeFinished}
* BatchListener {@link #batchStart} {@link BatchListener#batchNotify}
* {@link #batchStop} {@link BatchListener#batchNotify}
* </pre>
*
* @author GES
*/
public final class TransactionManager
{
/** The block won't start a transaction or sub-transaction. */
public static final int NO_TRANSACTION = 0;
/** The block will start a sub-transaction if a transaction is active. */
public static final int SUB_TRANSACTION = 1;
/** The block will start a transaction or sub-transaction. */
public static final int TRANSACTION = 2;
/** No <code>Finalizable</code> processing is currently occurring. */
public static final int OP_NONE = 0;
/** <code>entry</code> processing is currently occurring. */
public static final int OP_ENTRY = 1;
/** <code>iterate</code> processing is currently occurring. */
public static final int OP_ITERATE = 2;
/** <code>retry</code> processing is currently occurring. */
public static final int OP_RETRY = 3;
/** <code>finished</code> processing is currently occurring. */
public static final int OP_FINISHED = 4;
/** Indicates that the block has no properties. */
public static final int PROP_NONE = 0x00;
/** ERROR property. */
public static final int PROP_ERROR = 0x01;
/** ENDKEY property. */
public static final int PROP_ENDKEY = 0x02;
/** STOP property. */
public static final int PROP_STOP = 0x04;
/** QUIT property. */
public static final int PROP_QUIT = 0x08;
/**
* The index of the block where the automatic transaction will be opened (when a
* TRANSACTION-MODE AUTOMATIC language statement is in effect).
*/
private static final int APPSERVER_ROOT_BLOCK_INDEX = 0;
/** Instrumentation for {@link #processCommit}. */
private static final NanoTimer COMMIT = NanoTimer.getInstance(FwdServerJMX.TimeStat.TMCommit);
/** Instrumentation for {@link #processRollback}. */
private static final NanoTimer ROLLBACK = NanoTimer.getInstance(FwdServerJMX.TimeStat.TMRollback);
/** Instrumentation for {@link #processValidate}. */
private static final NanoTimer VALIDATE = NanoTimer.getInstance(FwdServerJMX.TimeStat.TMValidate);
/** Logger (this is JVM-wide rather than being context-local). */
private static final CentralLogger LOG = CentralLogger.get(TransactionManager.class.getName());
/** Context-local global data area for transaction state. */
private static final ContextContainer workArea = new ContextContainer();
/**
* Flags whether the {@code _Trans} metadata table is active. If the flag is active the active
* transactions for all users can be visualized using the {@code _Trans} SVT. Otherwise only
* the transactions for current user can be queried using {@code DBTASKID()} ABL function.
*/
private static final Map<String, TransactionTableUpdater> updaters = new CaseInsensitiveHashMap<>();
/** Transaction information for each database. Access to its content must be synchronized. */
private static final Map<String, DbTransactions> transactionData = new CaseInsensitiveHashMap<>();
/**
* Initialize a metaschema database by initializing a transaction listener and storing it for later use.
* <p>
* Called only at server startup.
*
* @param primaryDB
* Primary database for which metaschema information is being managed.
*/
public static void initializeMeta(Database primaryDB)
{
String dbId = primaryDB.getId();
// sync may not be mandatory here because the method is called during the initialization
synchronized (transactionData)
{
if (transactionData.containsKey(dbId))
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Multiple registration of '" + dbId + "' database.");
}
return;
}
transactionData.put(dbId, new DbTransactions());
if (MetadataManager.inUse(MetadataManager._TRANS) && TransactionTableUpdater.isEnabled())
{
updaters.put(dbId, new TransactionTableUpdater());
}
}
}
/**
* Generate a string representation of the given transaction level.
*
* @param level
* The transaction level to be decoded. Must be one of
* <code>NO_TRANSACTION</code>, <code>SUB_TRANSACTION</code> or
* <code>TRANSACTION</code>.
*
* @return The human readable string.
*/
public static String decodeLevel(int level)
{
String txt = "UNKNOWN";
switch (level)
{
case NO_TRANSACTION:
txt = "NO_TRANSACTION";
break;
case SUB_TRANSACTION:
txt = "SUB_TRANSACTION";
break;
case TRANSACTION:
txt = "TRANSACTION";
break;
}
return txt;
}
/**
* Generate a string representation of the block properties of a block.
*
* @param props
* The block properties to decode.
*
* @return The human readable string.
*/
public static String decodeProperties(int props)
{
StringBuilder sb = new StringBuilder();
if ((props & PROP_ERROR) != 0)
{
sb.append(BlockManager.Condition.ERROR.name());
}
if ((props & PROP_ENDKEY) != 0)
{
if (sb.length() > 0)
{
sb.append(", ");
}
sb.append(BlockManager.Condition.ENDKEY.name());
}
if ((props & PROP_STOP) != 0)
{
if (sb.length() > 0)
{
sb.append(", ");
}
sb.append(BlockManager.Condition.STOP.name());
}
if ((props & PROP_QUIT) != 0)
{
if (sb.length() > 0)
{
sb.append(", ");
}
sb.append(BlockManager.Condition.QUIT.name());
}
return sb.toString();
}
/**
* Reports on the current <code>Finalizable</code> operation being
* executed.
*
* @return One of the following:
* <ul>
* <li> <code>OP_NONE</code>
* <li> <code>OP_ITERATE</code>
* <li> <code>OP_RETRY</code>
* <li> <code>OP_FINISHED</code>
* </ul>
*/
public static int currentOperation()
{
return workArea.obtain().op;
}
/**
* Adds the given object into the list of objects for the current block
* which require commit notification. This form of registration is
* useful for objects whose natural lifetime are scoped to a different
* block than the block in which they must be committed. For example,
* if a global or shared resource needs to be committed in a specific
* scope, then this can be used in cooperation with the normal
* {@link #registerCurrent} processing to control the commit points.
* <p>
* This notification will ONLY occur when there is an active transaction.
* <p>
* Notifications occur in LIFO order; thus, commitables which depend upon
* others being committed first must be registered before the objects upon
* which they are dependent.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
*
* @param target
* The object reference to be committed on success. This
* cannot be <code>null</code>.
*/
public static void registerCommit(Commitable target)
{
WorkArea wa = workArea.obtain();
registerCommitAt(wa, wa.blocks.size() - 1, target);
}
/**
* Adds the given object into the list of objects which require commit notification for the
* block at the specified depth This form of registration is useful for objects whose natural
* lifetime are scoped to a different block than the block in which they must be committed.
* For example, if a global or shared resource needs to be committed in a specific scope,
* then this can be used in cooperation with the normal {@link #registerCurrent} processing to
* control the commit points.
* <p>
* This notification will ONLY occur when there is an active transaction.
* <p>
* Notifications occur in LIFO order; thus, commitables which depend upon
* others being committed first must be registered before the objects upon
* which they are dependent.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
*
* @param blockDepth
* Zero-based depth of the target block, starting from the outermost block.
* @param target
* The object reference to be committed on success. This cannot be
* <code>null</code>.
*/
public static void registerCommitAt(int blockDepth, Commitable target)
{
registerCommitAt(workArea.obtain(), blockDepth, target);
}
/**
* Adds the given object as the transaction level reference which will get
* a single, "master" commit call after all other commits have been
* processed at the full transaction level. This will never be called
* for sub-transaction commits, nor will it be called if the transaction
* is abnormally exited.
* <p>
* This notification will ONLY occur when there is an active transaction
* and it only will be called at the exit of the block that corresponds
* to the full transaction.
* <p>
* If the full transaction block is also an iterating block, this
* notification will occur for each iteration of the block.
*
* @param target
* The object reference to be committed on success. This
* can be <code>null</code> if one intends to clear the current
* reference.
* @param remove
* If <code>true</code> automatically remove this registration
* after the full transaction scope has ended (whether or not
* the commit occurs). If <code>false</code>, this reference
* will remain globally until replaced.
*/
public static void registerTransactionCommit(Commitable target, boolean remove)
{
WorkArea wa = workArea.obtain();
if (wa.masterCommit == null)
{
wa.masterCommit = new LinkedHashMap<>();
}
wa.masterCommit.put(target, remove ? Boolean.TRUE : Boolean.FALSE);
}
/**
* Register the given object as a handler which can veto the processing of
* STOP condition processing at the current scope. Handlers are invoked
* in FIFO order. Any handler can issue a veto, which forces the STOP
* condition to be handled at a higher scope, if at all.
*
* @param h
* The stop condition veto handler.
*/
public static void registerStopVetoHandler(StopConditionVetoHandler h)
{
WorkArea wa = workArea.obtain();
wa.stopHandlers.add(h);
}
/**
* Registers the instance for scope notifications at the current block.
* <p>
* This will call {@link Scopeable#scopeStart} to notify for the scope processing.
*
* @param target
* The scopeable instance.
*/
public static void registerBlockScopeable(Scopeable target)
{
registerBlockScopeable(workArea.obtain(), target);
}
/**
* Register the target scopeable as pending, to be added to the next block.
*
* @param target
* The scopeable instance.
*/
public static void registerPendingScopeable(Scopeable target)
{
registerPendingScopeable(workArea.obtain(), target);
}
/**
* Register the given <code>Resettable</code> object in order to reset it
* at specific points of block processing.
*
* @param resettable
* <code>Resettable</code> to be registered.
*/
public static void registerResettable(Resettable resettable)
{
WorkArea wa = workArea.obtain();
wa.resettables.add(resettable);
}
/**
* Adds the given object into the list of objects for the specified block
* which require iterate/retry/finish notification. This form of
* registration is useful for objects whose natural lifetime is scoped to
* a block and which need a hook for cleaning up resources or handling
* other block termination processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called)
* in the case of a non-global scope OR at the moment that the last
* scope has been removed in the case of the global scope.
* <p>
* The given object will only be added if it is not already in the list.
* <p>
* Finalizables for a scope are processed in the same order as they were
* registered.
*
* @param target
* The object reference to be notified on any exit from the
* block specified. This cannot be <code>null</code>.
* @param global
* Specifies the target scope. <code>false</code> specifies the
* current scope, and <code>true</code> specifies the global
* scope.
*/
public static void registerFinalizable(Finalizable target, boolean global)
{
WorkArea wa = workArea.obtain();
registerFinalizableAt(wa, (global ? 0 : wa.blocks.size() - 1), target);
}
/**
* Remove the given finalizable from the global list of finalizables.
*
* @param target
* Finalizable to be removed from the global list.
*/
public static void deregisterGlobalFinalizable(Finalizable target)
{
deregisterGlobalFinalizable(workArea.obtain(), target);
}
/**
* Adds the given object into the list of objects for the specified block
* which require iterate/retry/finish notification. This form of
* registration is useful for objects whose natural lifetime is scoped to
* a block and which need a hook for cleaning up resources or handling
* other block termination processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called)
* in the case of a non-global scope OR at the moment that the last
* scope has been removed in the case of the global scope.
* <p>
* The given object will only be added if it is not already in the list.
* <p>
* Finalizables for a scope are processed in the same order as they were
* registered.
*
* @param blockDepth
* zero-based depth of target block, starting from the outermost
* block. <code>0</code> indicates the global block.
* @param target
* The object reference to be notified on any exit from the
* block specified. This cannot be <code>null</code>.
*/
public static void registerFinalizableAt(int blockDepth, Finalizable target)
{
registerFinalizableAt(workArea.obtain(), blockDepth, target);
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notifications for the next block that is opened
* which is a top-level block that corresponds to a Progress external
* procedure.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block and which need a hook for cleaning up
* resources or handling other block termination processing. In
* particular, it is necessary for objects that are constructed during
* a class' constructor/initializer (before the external procedure block
* has opened). Such objects cannot simply register since their
* construction is not actually enclosed by the block.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
*
* @param target
* The object reference to be notified on any exit from the
* top-level external block. This cannot be <code>null</code>.
*
* @throws IllegalStateException
* if the top-level block cannot be identified.
*/
public static void registerNextExternal(Finalizable target)
{
registerNextExternal(workArea.obtain(), target);
}
/**
* Allow finalizables to do their cleanup ({@link Finalizable#initFailure()}) now since the
* scoped they were designed was aborted and there won't be other chance to call their
* {@link Finalizable#finished()}.
* <p>
* After iteration the list is deleted.
* <p>
* Also, cleanup all other pending state being collected while the external program was
* instantiating.
*/
public static void cleanupPending()
{
cleanupPending(workArea.obtain());
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notifications for the nearest enclosing
* top-level block. A top-level block is generated for Progress external
* procedures, internal procedures, triggers and functions. An additional
* parameter allows registration to be specified for the specific block
* which corresponds with an external procedure block scope in Progress.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block and which need a hook for cleaning up
* resources or handling other block termination processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
* <p>
* Finalizables for a scope are processed in the same order as they were
* registered.
*
* @param target
* The object reference to be notified on any exit from the
* top-level block. This cannot be <code>null</code>.
* @param external
* If <code>true</code>, register with the nearest enclosing
* external block (external procedure), otherwise register with
* the nearest enclosing top-level block (which may be but is
* not guaranteed to be the external block).
*
* @throws IllegalStateException
* if the top-level block cannot be identified.
*/
public static void registerTopLevelFinalizable(Finalizable target, boolean external)
{
registerTopLevelFinalizable(workArea.obtain(), target, external);
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notification for the nearest enclosing, looping
* block.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the nearest enclosing, looping block and which need a hook
* for cleaning up resources or handling other block termination
* processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
*
* @param target
* The object reference to be notified on any exit from or
* iteration of the target block. This cannot be
* <code>null</code>.
*
* @throws IllegalStateException
* if no enclosing, looping block can be identified.
*/
public static void registerNearestLoopFinalizable(Finalizable target)
{
registerNearestLoopFinalizable(workArea.obtain(), target);
}
/**
* Removes the given object from the list of objects which require
* iterate/retry/finish notifications. This will find the first instance
* of the given reference by walking up each block from the current to the
* global block. The first instance found will be removed and the method
* will then return. If the same instance is registered in multiple scopes,
* it would take multiple calls to remove all instances.
* <p>
* After this call, no notifications will be received.
*
* @param target
* The object reference to be removed.
*
* @return <code>true</code> if the target instance was found and
* removed.
*/
public static boolean removeFinalizable(Finalizable target)
{
if (target == null)
{
// nothing to do
return false;
}
return removeFinalizable(workArea.obtain(), target);
}
/**
* Adds the given object as the transaction level reference which will get
* a "master" finish call after all other finish notifications have been
* processed at the full transaction level. This will never be called for
* sub-transaction finish notification.
* <p>
* This notification will ONLY occur when there is an active transaction
* and it only will be called at the exit of the block that corresponds
* to the full transaction.
* <p>
* Multiple targets may be registered by invoking this method multiple
* times. However, the same object registered multiple times will receive
* the notification only once per full transaction block exit. If multple
* targets are registered, they will be called in the order in which they
* were registered.
*
* @param target
* The object reference to be finished at full transaction block
* end.
* @param remove
* If <code>true</code> automatically remove this registration
* after the full transaction scope has ended. If
* <code>false</code>, this reference will remain globally until
* replaced.
*/
public static void registerTransactionFinish(Finalizable target, boolean remove)
{
WorkArea wa = workArea.obtain();
if (wa.masterFinish == null)
{
wa.masterFinish = new LinkedHashMap<>();
}
wa.masterFinish.put(target, remove);
}
/**
* Adds the given object into the batch notifier list. This is a global
* list (it isn't associated with any block) that is triggered externally
* by a call to {@link #batchStart} and {@link #batchStop}. Every object
* in the list will be notified in the order in which they were added to
* the list.
*
* @param target
* The object reference to be notified of any batch start and
* stop.
*/
public static void registerBatchListener(BatchListener target)
{
workArea.obtain().batchListeners.add(target);
}
/**
* Reports whether the current block was just rolled back or a containing
* block has a pending rollback. If there is no current transaction open,
* then there cannot be a pending or current rollback.
*
* @return <code>true</code> if there was or will be a rollback.
*/
public static boolean hadRollback()
{
return hadRollback(workArea.obtain());
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the scope defined by the
* current block. All objects that are rolled back will have their
* values assigned based on the values backed up at the start of the
* specified scope. A flow control change MUST always immediately follow
* this method otherwise undefined results can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*/
public static void rollback()
{
rollback(null);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the scope defined
* explicitly by the given label or implicitly if the label is
* <code>null</code>. All objects that are rolled back will have their
* values assigned based on the values backed up at the start of the
* specified scope. If the current scope is not the target scope, the
* given exception will be stored, the rollback will be deferred and
* the rollback will be processed as the scopes naturally unwind. This
* unwinding must be generated by a change in flow of control by the
* calling code. This flow control change MUST always immediately follow
* this method otherwise undefined results can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*
* @param label
* Specifies the name of the scope at which to rollback or
* <code>null</code> if the scope should be implicitly
* determined.
*/
public static void rollback(String label)
{
rollback(label, null);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the scope defined
* explicitly by the given label or implicitly if the label is
* <code>null</code>. All objects that are rolled back will have their
* values assigned based on the values backed up at the start of the
* specified scope. If the current scope is not the target scope, the
* given exception will be stored, the rollback will be deferred and
* the rollback will be processed as the scopes naturally unwind. This
* unwinding must be generated by a change in flow of control by the
* calling code. This flow control change MUST always immediately follow
* this method otherwise undefined results can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*
* @param label
* Specifies the name of the scope at which to rollback or
* <code>null</code> if the scope should be implicitly
* determined.
* @param cause
* The exceptional cause, if any, of the rollback.
*/
public static void rollback(String label, Throwable cause)
{
rollback(workArea.obtain(), label, cause);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the nearest top level scope.
* All objects that are rolled back will have their values assigned based
* on the values backed up at the start of the specified scope. If the
* current scope is not the target scope, the given exception will be
* stored, the rollback will be deferred and the rollback will be processed
* as the scopes naturally unwind. This unwinding must be generated by a
* change in flow of control by the calling code. This flow control change
* MUST always immediately follow this method otherwise undefined results
* can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*/
public static void rollbackTopLevel()
{
rollbackTopLevel(workArea.obtain());
}
/**
* Initializes a new scope, the associated block definition data
* structures. A backup of objects that can be rolled back is deferred
* until after the block opens (see {@link #blockSetup} and
* {@link #makeBackup}).
* <p>
* In the case of a block which is defined as <code>NO_TRANSACTION</code>,
* the core data structures are initialized but no rollback support will
* be provided.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}.
* <p>
* This will create a block of <code>BlockType.UNKNOWN</code> and block
* properties <code>PROP_NONE</code>.
*
* @param label
* The text label associated with the block. This usually
* should correspond with the label used in the source file
* to identify the block for purposes of the break and continue
* statements.
* @param level
* One of {@link #NO_TRANSACTION}, {@link #SUB_TRANSACTION} or
* {@link #TRANSACTION}. This code defines the level of
* rollback and transaction support which should be provided.
* @param external
* <code>true</code> if this block is associated with the
* external procedure (in Progress terms). This controls the
* cleanup of certain resources such as temp-tables, frames
* and streams.
* @param topLevel
* <code>true</code> if this block is a method.
* @param loop
* Defines if this block is a loop or not.
* @param next
* <code>true</code> if this infinite loop protection should
* convert a retry into a next (continue), otherwise the retry
* will be converted into a leave (break).
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public static void pushScope(String label,
int level,
boolean external,
boolean topLevel,
boolean loop,
boolean next)
{
pushScope(label,
level,
PROP_NONE,
external,
topLevel,
loop,
next,
UNKNOWN);
}
/**
* Returns the label of the current block scope.
*
* @return See above.
*/
public static String getScopeLabel()
{
Stack<BlockDefinition> blocks = workArea.obtain().blocks;
return blocks.size() > 0 ? blocks.peek().label : null;
}
/**
* Initializes a new scope, the associated block definition data
* structures. A backup of objects that can be rolled back is deferred
* until after the block opens (see {@link #blockSetup} and
* {@link #makeBackup}).
* <p>
* In the case of a block which is defined as <code>NO_TRANSACTION</code>,
* the core data structures are initialized but no rollback support will
* be provided.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}.
* <p>
* This will create a block with block properties <code>PROP_NONE</code>.
*
* @param label
* The text label associated with the block. This usually
* should correspond with the label used in the source file
* to identify the block for purposes of the break and continue
* statements.
* @param level
* One of {@link #NO_TRANSACTION}, {@link #SUB_TRANSACTION} or
* {@link #TRANSACTION}. This code defines the level of
* rollback and transaction support which should be provided.
* @param external
* <code>true</code> if this block is associated with the
* external procedure (in Progress terms). This controls the
* cleanup of certain resources such as temp-tables, frames
* and streams.
* @param topLevel
* <code>true</code> if this block is a method.
* @param loop
* Defines if this block is a loop or not.
* @param next
* <code>true</code> if this infinite loop protection should
* convert a retry into a next (continue), otherwise the retry
* will be converted into a leave (break).
* @param blockType
* Specifies this block's type.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public static void pushScope(String label,
int level,
boolean external,
boolean topLevel,
boolean loop,
boolean next,
BlockType blockType)
{
pushScope(label,
level,
PROP_NONE,
external,
topLevel,
loop,
next,
blockType);
}
/**
* Initializes a new scope, the associated block definition data
* structures. A backup of objects that can be rolled back is deferred
* until after the block opens (see {@link #blockSetup} and
* {@link #makeBackup}).
* <p>
* In the case of a block which is defined as <code>NO_TRANSACTION</code>,
* the core data structures are initialized but no rollback support will
* be provided.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @param label
* The text label associated with the block. This usually
* should correspond with the label used in the source file
* to identify the block for purposes of the break and continue
* statements.
* @param level
* One of {@link #NO_TRANSACTION}, {@link #SUB_TRANSACTION} or
* {@link #TRANSACTION}. This code defines the level of
* rollback and transaction support which should be provided.
* @param props
* A bitfield storing block properties which will be a
* combination of {@link #PROP_NONE}, {@link #PROP_ERROR},
* {@link #PROP_ENDKEY}, {@link #PROP_STOP} or
* {@link #PROP_QUIT}.
* @param external
* <code>true</code> if this block is associated with the
* external procedure (in Progress terms). This controls the
* cleanup of certain resources such as temp-tables, frames
* and streams.
* @param topLevel
* <code>true</code> if this block is a method.
* @param loop
* Defines if this block is a loop or not.
* @param next
* <code>true</code> if this infinite loop protection should
* convert a retry into a next (continue), otherwise the retry
* will be converted into a leave (break).
* @param blockType
* Specifies this block's type.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public static void pushScope(String label,
int level,
int props,
boolean external,
boolean topLevel,
boolean loop,
boolean next,
BlockType blockType)
{
WorkArea wa = workArea.obtain();
pushScope(wa,
label,
level,
props,
external,
topLevel,
loop,
next,
false,
blockType);
}
/**
* Remove the current block from the stack of scopes, process any pending
* rollbacks (if the current block is the proper target), notify any
* objects needing commits (if the block finished successfully) and handle
* finish processing for the block. This method must be called from a
* <code>finally</code> block such that this is guaranteed to be called
* no matter how the flow of control exits the associated block (exception,
* break, return or the flow of control naturally passing through the
* end of the block).
* <p>
* Rollback and commit processing only occur when there is an active
* transaction. Otherwise this processing is bypassed. Finish processing
* is always called.
* <p>
* This method implements global finish processing (as the last action
* before returning) when the last scope is popped from the stack of
* scopes. For this reason, the current implementation assumes that the
* session is over at the moment that the top-most user-defined scope
* exits.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public static void popScope()
throws StopConditionException
{
popScope(workArea.obtain());
}
/**
* Notify {@link ConditionListener} instances about condition.
*
* @param condition
* The condition the listeners will be notified.
*/
public static void notifyCondition(BlockManager.Condition condition)
{
// do not pass null down
if (condition == null)
{
return;
}
notifyCondition(workArea.obtain(), condition);
}
/**
* Notify {@link ConditionListener} instances about exception condition.
*
* @param condition
* The instance of {@link ConditionException}.
*/
public static void notifyCondition(ConditionException condition)
{
notifyCondition(BlockManager.exceptionToCondition(condition));
}
/**
* Check if the latest block has initialized.
*
* @return See above.
*/
public static boolean blockInitialized()
{
BlockDefinition blk = workArea.obtain().blocks.peek();
return blk.init;
}
/**
* Get a helper to access certain context-local state required for undoable support.
*/
public static UndoableHelper getUndoableHelper()
{
return new UndoableHelper(workArea.obtain());
}
/**
* Get a helper to access certain context-local state required for transaction support.
*/
public static TransactionHelper getTransactionHelper()
{
return new TransactionHelper(workArea.obtain());
}
/**
* Provides a callback entry point for the top of the loop. This location
* is required to handle the backup of undo variables via
* {@link #makeBackup}. On the second and subsequent executions of this
* method for a given block, this method handles <code>iterate</code>
* notifications.
* <p>
* It is important to note that <code>continue</code> can be called from
* nested blocks at arbitrary depths. This method can handle such nesting
* at any level.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public static void blockSetup()
throws StopConditionException
{
blockSetup(workArea.obtain());
}
/**
* Creates or updates the backup set for all objects needing rollback
* support in the current block, if a transaction is active. This is
* split up from {@link #pushScope} and {@link #blockSetup} due to the
* semantics of Progress in which the backup set is created or refreshed
* after the loop control variables have been modified.
*/
public static void makeBackup()
{
backupWorker(workArea.obtain(), null);
}
/**
* Reports if there is a current transaction which is active.
*
* @return <code>true</code> if a transaction is active.
*/
public static logical isTransactionActive()
{
return new logical(isTransaction());
}
/**
* Reports if the current iteration of this block or loop is a retry.
*
* @return <code>true</code> if this is a retry of this block or loop
* (as opposed to the first time through this iteration).
*/
public static boolean _isRetry()
{
return _isRetry(workArea.obtain());
}
/**
* Reports if the current iteration of this block or loop is a retry.
*
* @return <code>true</code> if this is a retry of this block or loop
* (as opposed to the first time through this iteration).
*/
public static logical isRetry()
{
return new logical(_isRetry());
}
/**
* Records the fact that the pending RETRY is being caused by an ENDKEY
* condition. This enables "downstream" retry processing to implement the
* "stuttering" quirk and some behavior in honoring PAUSE as a way to
* disable infinite loop protection.
*/
public static void markEndkeyRetry()
{
markEndkeyRetry(workArea.obtain());
}
/**
* Disables infinite loop protection for the remainder of the current
* iteration of the innermost block. This should be called from any
* runtime code that blocks for user input AND from the {@link #isRetry}
* method.
*
* @param blk
* The block on which to operator or <code>null</code> to
* operate on the current (innermost) block.
*/
public static void disableLoopProtection(BlockDefinition blk)
{
if (blk == null)
{
// peek at the current block def on the top of the stack
blk = workArea.obtain().blocks.peek();
}
blk.loopProtection = false;
}
/**
* This checks if any of the enclosing loops has iterated at least once.
*
* @return <code>true</code> if any of the enclosing loops has iterated
* at least once.
*/
public static boolean hasParentIteratedBlock()
{
return hasParentIteratedBlock(workArea.obtain());
}
/**
* Determines if a NEXT operation with the given target block should
* be honored or if the NEXT should be converted to a LEAVE. This is
* a special form of infinite loop protection.
* <p>
* A NEXT is converted into a LEAVE when all of the following conditions
* are true:
* <p>
* <ul>
* <li> The NEXT is associated with the UNDO, NEXT language statement or
* with an ON phrase that has UNDO, NEXT as the result of the given
* condition (e.g. ON ENDKEY UNDO, NEXT). It is important to note
* that Progress does not provide infinite loop protection for the
* NEXT language statement, so there is no conversion to LEAVE in
* that case.
* <li> The target block (whether implicitly determined by the lack of a
* label or explicitly determined by the presence of a label) must
* also have been the target block for the UNDO operation. It is
* possible for the NEXT to be targetted at a more enclosing block
* than the UNDO, in which case there is no conversion to LEAVE.
* <li> The target block type must be REPEAT, REPEAT WHILE or DO WHILE.
* These are the only loop types that normally convert RETRY to
* LEAVE instead of NEXT. Likewise, these are the only loop types
* which honor infinite loop protection for NEXT by converting it
* to a LEAVE.
* <li> Infinite loop protection must not have been disabled for the
* target block. In this case, any form of PAUSE is honored as
* one of the mechanisms to disable infinite loop protection.
* </ul>
* <p>
* Since NEXT is only possible on a looping block, if the target block
* is not a looping block then this method will return <code>false</code>.
*
* @param label
* The label of the target block for the NEXT action.
*
* @return <code>true</code> for NEXT, <code>false</code> for LEAVE.
*/
public static boolean honorNext(String label)
{
return honorNext(workArea.obtain(), label);
}
/**
* Determines if the currently executing block should be retried. This
* is where infinite loop protection is implemented if it is enabled.
*
* @return <code>true</code> if the block (whether or not it is a loop)
* should be retried.
*/
public static boolean needsRetry()
{
return needsRetry(workArea.obtain());
}
/**
* Accesses the state of whether there is a pending break that must be
* honored. This would have been generated by the infinite loop
* protection.
*
* @return <code>true</code> if a break is pending for this block.
*/
public static boolean isBreakPending()
{
return isBreakPending(workArea.obtain());
}
/**
* Force an error to be generated such that it will be caught in the
* caller of this method. The stack will be unwound and normal error
* processing bypassed until this method returns. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method,
* the "normal" stack unwinding approach cannot be used because the
* exception cannot be thrown. Instead, the error manager will be updated
* with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a
* <code>return</code> statement which will maintain the proper Progress
* behavior. The caller can check the error manager to determine that an
* error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top
* level block be found. The silent error flag of the caller is cached
* there.
*
* @param text
* The text to use as the error message in the exception.
*
* @throws ErrorConditionException
*/
public static void triggerErrorInCaller(String text)
throws ErrorConditionException
{
triggerErrorInCaller(workArea.obtain(), null, text);
}
/**
* Force an error to be generated such that it will be caught in the caller of this method. The
* stack will be unwound and normal error processing bypassed until this method returns. This
* unwinding is dependent upon catch blocks being implemented with a corresponding call to
* {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method, the "normal" stack
* unwinding approach cannot be used because the exception cannot be thrown. Instead, the error
* manager will be updated with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a {@code return}
* statement which will maintain the proper Progress behavior. The caller can check the error
* manager to determine that an error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top level block be found.
* The silent error flag of the caller is cached there.
* <p>
* NOTE: The error message will NOT be printed if the calling procedure is the 'main' one.
*
* @param errNum
* The identifier of the error.
* @param text
* The text to use as the error message in the exception.
*
* @throws ErrorConditionException
*/
public static void triggerErrorInCaller(int errNum, String text)
throws ErrorConditionException
{
triggerErrorInCaller(workArea.obtain(), errNum, text);
}
/**
* Force an error to be generated such that it will be caught in the caller of this method. The
* stack will be unwound and normal error processing bypassed until this method returns. This
* unwinding is dependent upon catch blocks being implemented with a corresponding call to
* {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method, the "normal" stack
* unwinding approach cannot be used because the exception cannot be thrown. Instead, the error
* manager will be updated with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a {@code return}
* statement which will maintain the proper Progress behavior. The caller can check the error
* manager to determine that an error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top level block be found.
* The silent error flag of the caller is cached there.
* <p>
* NOTE: The error message will NOT be printed if the calling procedure is the 'main' one.
*
* @param errNum
* The identifier of the error.
* @param text
* The text to use as the error message in the exception.
* @param errFlag
* Flag indicating the ERROR-ERROR:ERROR is set.
* @param prefix
* Prefix or not with two asterics when building the error message text.
*
* @throws ErrorConditionException
*/
public static void triggerErrorInCaller(int errNum, String text, boolean errFlag, boolean prefix)
throws ErrorConditionException
{
triggerErrorInCaller(workArea.obtain(), errNum, text, errFlag, prefix);
}
/**
* Check if this block's caller requires errors to be suppressed.
*
* @return {@link BlockDefinition#suppressError} for {@link #nearestTopLevel} block.
*/
public static boolean isSuppressError()
{
return isSuppressError(workArea.obtain());
}
/**
* Force an error to be generated such that it will be caught in the
* caller of this method. The stack will be unwound and normal error
* processing bypassed until this method returns. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method,
* the "normal" stack unwinding approach cannot be used because the
* exception cannot be thrown. Instead, the error manager will be updated
* with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a
* <code>return</code> statement which will maintain the proper Progress
* behavior. The caller can check the error manager to determine that an
* error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top
* level block be found. The silent error flag of the caller is cached
* there.
*
* @throws ErrorConditionException
*/
public static void triggerErrorInCaller()
throws ErrorConditionException
{
triggerErrorInCaller("");
}
/**
* Force a retry of the current block (if there is no pending rollback)
* or retry the same block as the pending rollback, in either case
* ensure that the flow of control will not naturally continue.
* <p>
* A {@link RetryUnwindException} will be thrown to unwind the stack
* to the given scope, even if the scope is the current block. This
* unwinding is dependent upon catch blocks being implemented with a
* corresponding call to {@link #honorRetry}.
* <p>
* <b>This method will NEVER naturally return since an exception is ALWAYS
* thrown.</b> This method should generally not be called from within a
* catch block if the intention is to retry the current block since it
* would be outside of the block's associated <code>try</code>. In that
* case, only an enclosing block could be retried.
*
* @throws RetryUnwindException
* This method always generates this exception to ensure that
* natural processing will not continue.
*/
public static void forceRetry()
throws RetryUnwindException
{
forceRetry(null);
}
/**
* Force a retry of the current block or the scope named by the given
* label, ensuring that the flow of control will not naturally continue.
* A {@link RetryUnwindException} will be thrown to unwind the stack
* to the given scope, even if the scope is the current block. This
* unwinding is dependent upon catch blocks being implemented with a
* corresponding call to {@link #honorRetry}.
* <p>
* <b>This method will NEVER naturally return since an exception is ALWAYS
* thrown.</b> This method should generally not be called from within a
* catch block if the intention is to retry the current block since it
* would be outside of the block's associated <code>try</code>. In that
* case, only an enclosing block could be retried.
*
* @param label
* The name of the scope to retry. Specify <code>null</code> to
* retry the scope that has a pending rollback OR to retry
* the current scope if there is no pending rollback.
*
* @throws RetryUnwindException
* This method always generates this exception to ensure that
* natural processing will not continue.
*/
public static void forceRetry(String label)
throws RetryUnwindException
{
forceRetry(workArea.obtain(), label);
}
/**
* Force a retry of the current block (if there is no pending rollback)
* or the block targeted for a pending rollback. If a containing scope is
* the target of the retry, then a {@link RetryUnwindException} will be
* thrown to unwind the stack to the given scope. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorRetry}.
* <p>
* If the current block is not the target, the retry is deferred by
* marking the target block and then unwinding the stack. Otherwise,
* this method will return naturally. This method is only appropriate
* to be called from within <code>catch</code> blocks since a natural
* return within a <code>catch</code> block will fall through to allow
* the retry loop to engage.
*
* @throws RetryUnwindException
*/
public static void triggerRetry()
throws RetryUnwindException
{
triggerRetry(null);
}
/**
* Force a retry of the current block or the scope named by the given
* label. If a containing scope is the target of the retry, then
* a {@link RetryUnwindException} will be thrown to unwind the stack
* to the given scope. This unwinding is dependent upon catch blocks
* being implemented with a corresponding call to {@link #honorRetry}.
* <p>
* If the current block is not the target, the retry is deferred by
* marking the target block and then unwinding the stack. Otherwise,
* this method will return naturally. This method is only appropriate
* to be called from within <code>catch</code> blocks since a natural
* return within a <code>catch</code> block will fall through to allow
* the retry loop to engage.
*
* @param label
* The name of the scope to retry. Specify <code>null</code> to
* retry the scope of that has a pending rollback OR to retry
* the current scope if there is no pending rollback.
*
* @throws RetryUnwindException
*/
public static void triggerRetry(String label)
throws RetryUnwindException
{
triggerRetry(workArea.obtain(), label);
}
/**
* Dispatch the given exception to all registered stop condition handlers
* to see if any requires that the STOP condition be vetoed at the
* current scope. If any of the registered handlers indicate that the STOP
* condition should not be honored at this level, the exception is
* rethrown, presumably to be honored at a higher scope, or not at all. In
* the latter case, the program running in the current context will abort.
*
* @param exc
* The exception to be checked.
*
* @throws StopConditionException
* if the STOP condition is not honored at this scope.
*/
public static void honorStop(StopConditionException exc)
throws StopConditionException
{
honorStop(workArea.obtain(), exc);
}
/**
* Check if there is a pending retry of the current block and if so then
* enable retry. This is the "last half" of the processing to unwind the
* stack up to a containing scope that is the target of a retry. The
* "first half" of the unwinding is implemented by throwing a
* {@link RetryUnwindException}. This unwinding is dependent upon catch
* blocks being implemented to call this method.
* <p>
* If the current block is not the target for the pending retry, the
* exception is re-thrown.
*
* @param rue
* The exception to honor or ignore.
*
* @throws RetryUnwindException
*/
public static void honorRetry(RetryUnwindException rue)
throws RetryUnwindException
{
honorRetry(workArea.obtain(), rue);
}
/**
* Check if the current error must unwind all the way to the caller of
* the top-level block in this method (if the ignore flag is set). If
* this is the case then the error is re-thrown. Before this occurs, if
* the current block is the top-level block of this method, then the
* ignore flag will be cleared. If the ignore flag is <code>false</code>
* then this method returns silently.
* <p>
* This code also honors any nested error that was generated while another
* error was being processed AND while nested error mode was enabled. Any
* such stored error will be re-thrown here.
*
* @param err
* The exception to honor or ignore.
*
* @throws ErrorConditionException
*/
public static void honorError(ErrorConditionException err)
throws ErrorConditionException
{
honorError(workArea.obtain(), err);
}
/**
* Notifies the transaction manager that an unexpected exception or error
* has occurred and that any transaction must be aborted. This will
* maintain the state of the system properly, including rolling back any
* open transaction. Once the state is properly maintained, the triggering
* <code>Throwable</code> will be re-thrown as a
* <code>RuntimeException</code>.
* <p>
* If the exception is of type <code>ConditionException</code> or
* <code>RetryUnwindException</code>, this method will do nothing since
* these are used for normal flow of control purposes and are used in
* close coordination with the transaction manager's state. They need
* no other special behavior to be safe. This method is designed to
* handle exceptions (like <code>NullPointerException</code>) which are
* not valid in a converted 4GL program.
*
* @param thr
* The unexpected exception or error.
*
* @throws RuntimeException
*/
public static void abnormalEnd(Throwable thr)
throws RuntimeException
{
abnormalEnd(thr, true);
}
/**
* Notifies the transaction manager that an unexpected exception or error
* has occurred and that any transaction must be aborted. This will
* maintain the state of the system properly, including rolling back any
* open transaction. Once the state is properly maintained, the triggering
* <code>Throwable</code> will be re-thrown as a
* <code>RuntimeException</code>.
* <p>
* If the exception is of type <code>ConditionException</code> or
* <code>RetryUnwindException</code>, this method will do nothing since
* these are used for normal flow of control purposes and are used in
* close coordination with the transaction manager's state. They need
* no other special behavior to be safe. This method is designed to
* handle exceptions (like <code>NullPointerException</code>) which are
* not valid in a converted 4GL program.
*
* @param thr
* The unexpected exception or error.
* @param needsNotify
* <code>true</code> if condition notification must be executed
* (only happens if the <code>thr</code> is a condition).
*
* @throws RuntimeException
*/
public static void abnormalEnd(Throwable thr, boolean needsNotify)
throws RuntimeException
{
abnormalEnd(workArea.obtain(), thr, needsNotify);
}
/**
* Reports if the given transaction level represents the case where the
* block has no effect on transaction processing.
*
* @param lvl
* The transaction level to test.
*
* @return <code>true</code> if the level is <code>NO_TRANSACTION</code>.
*/
public static boolean isNoTransactionLevel(int lvl)
{
return (lvl == NO_TRANSACTION);
}
/**
* Reports if the given transaction level represents the case where the
* block has creates a sub-transaction when a transaction is active.
*
* @param lvl
* The transaction level to test.
*
* @return <code>true</code> if the level is
* <code>SUB_TRANSACTION</code>.
*/
public static boolean isSubTransactionLevel(int lvl)
{
return (lvl == SUB_TRANSACTION);
}
/**
* Reports if the given transaction level represents the case where the
* block has creates a new transaction if a transaction is not active or
* creates a sub-transaction when a transaction is active.
*
* @param lvl
* The transaction level to test.
*
* @return <code>true</code> if the level is <code>TRANSACTION</code>.
*/
public static boolean isTransactionLevel(int lvl)
{
return (lvl == TRANSACTION);
}
/**
* Reports the transaction level of the current block.
*
* @return The transaction level.
*/
public static int currentTransactionLevel()
{
BlockDefinition blk = workArea.obtain().blocks.peek();
return blk.level;
}
/**
* Reports if the current scope defines the outermost transaction block
* (which is the scope at which final commit processing must occur).
*
* @return <code>true</code> if the current scope defines the top-level
* transaction.
*/
public static boolean isFullTransaction()
{
return workArea.obtain().isFullTransaction();
}
/**
* Reports if there is a current transaction which is active.
*
* @return <code>true</code> if a transaction is active.
*/
public static boolean isTransaction()
{
return workArea.obtain().isTransaction();
}
/**
* Obtain the current transaction id for a database (DBTASKID function).
* <p>
* The {@code DBTASKID} function is related to _trans VST. However, the relation is not 1:1.
* If, in a transaction, any record from a database is involved (updated OR {@code FIND},
* regardless of the lock level - and this includes the {@code _Trans} VST, too) then the task
* (transaction) id is incremented and the new value is returned by {@code DBTASKID} function.
* Otherwise (all records of the database are not accessed in any way OR just read access to
* buffers loaded before the transaction started) the {@code _Trans._Trans-Num} of the database
* is NOT incremented and {@code DBTASKID} function returns {@code ?} (unknown value).
* <p>
* In other words, even if the transaction block is already being executed, {@code DBTASKID}
* function (so this method) returns unknown value for a database, until at least a field is
* updated or a buffer is loaded (with {@code FIND} or {@code for}).
*
* @param db
* The key name of the database.
*
* @return The the current transaction id of the database or {@code null} if the database is
* unknown / not connected or if the transaction id has not been set yet.
*/
public static Integer getTransactionId(String db)
{
// no need to check whether there is a transaction; the direct map lookup is much faster
DbTransactions dbData;
// this might not be necessary. The [transactionData] is only populated during dbs init
synchronized (transactionData)
{
dbData = transactionData.get(db);
if (dbData == null)
{
return null; // no known data about this database
}
}
synchronized (dbData)
{
SecurityManager sm = SecurityManager.getInstance();
if (sm == null)
{
return null; // a bit strange, should not happen
}
Integer userId = sm.sessionSm.getSessionId();
if (userId == null)
{
return null; // a bit strange, should not happen
}
return dbData.transactions.get(userId); // may be null if a transaction is not active
}
}
/**
* Flushes all known content of {@code _Trans} metadata table to database. This is done before
* every access to {@code _Trans} table to make sure the fresh data is accessible by the SQL.
*
* @param p
* The persistence to be used.
*/
public static void flushTransMetaData(Persistence p)
{
if (!TransactionTableUpdater.isEnabled())
{
return; // the _Trans metadata table is not active
}
DbTransactions dbData;
TransactionTableUpdater updater;
synchronized (transactionData)
{
if (updaters.isEmpty())
{
return; // quick out
}
String dbName = p.getDatabase(Persistence.META_CTX).getId();
updater = updaters.get(dbName);
if (updater == null)
{
return; // the _Trans metadata table is not enabled for this database
}
dbData = transactionData.get(dbName);
if (dbData == null || !dbData.isDirty)
{
// no known data about this database. We can safely return here, there was no
// transaction activity for this database.
return;
}
}
synchronized (dbData)
{
updater.flush(p, dbData.transactions);
// mark database as clean (states for all transactions were flushed)
dbData.isDirty = false;
}
}
/**
* Reports if there is a current active transaction at the specified block depth.
*
* @param blockDepth
* Zero-based block depth starting from the outermost block at which transaction
* status is checked.
*
* @return <code>true</code> if a transaction is active at the specified block depth.
*/
public static boolean isTransactionAt(int blockDepth)
{
int trans = workArea.obtain().transLevel;
return trans != -1 && trans <= blockDepth;
}
/**
* Reports if the current block is a top-level block (this corresponds
* with a method-level block).
*
* @return <code>true</code> if top-level block, else <code>false</code>.
*/
public static boolean isTopLevelBlock()
{
return isTopLevelBlock(workArea.obtain());
}
/**
* Reports if the current block is a external block (this corresponds
* with a method-level block that was derived from a Progress external
* procedure). This will be <code>false</code> for blocks derived from
* internal procedures, functions and triggers.
*
* @return <code>true</code> if it is an external block, else
* <code>false</code>.
*/
public static boolean isExternalBlock()
{
BlockDefinition blk = workArea.obtain().blocks.peek();
return blk.external;
}
/**
* Reports if the current block is a global scoped block. For normal contexts:
* <code>true</code> if the current block is the only block on the stack of block scopes; for
* appserver agent contexts: <code>true</code> if the current block is one of two outermost
* blocks ("appserver-agent" and "startup") on the stack of block scopes.
*
* @return See above.
*/
public static boolean isGlobalBlock()
{
WorkArea wa = workArea.obtain();
return isGlobalBlock(wa);
}
/**
* Reports if the current block has the ERROR property.
*
* @return <code>true</code> if the current block has the
* <code>PROP_ERROR</code> bit set in its properties bitmap.
*/
public static boolean hasErrorProp()
{
return hasProperty(workArea.obtain(), PROP_ERROR);
}
/**
* Reports if the current block has the ENDKEY property.
*
* @return <code>true</code> if the current block has the
* <code>PROP_ENDKEY</code> bit set in its properties bitmap.
*/
public static boolean hasEndkeyProp()
{
return hasProperty(workArea.obtain(), PROP_ENDKEY);
}
/**
* Reports if the current block has the STOP property.
*
* @return <code>true</code> if the current block has the
* <code>PROP_STOP</code> bit set in its properties bitmap.
*/
public static boolean hasStopProp()
{
return hasProperty(workArea.obtain(), PROP_STOP);
}
/**
* Reports if the current block has the QUIT property.
*
* @return <code>true</code> if the current block has the
* <code>PROP_QUIT</code> bit set in its properties bitmap.
*/
public static boolean hasQuitProp()
{
return hasProperty(workArea.obtain(), PROP_QUIT);
}
/**
* During global finish processing, this reports if the exit was caused
* by a <code>QuitConditionException</code>.
*
* @return <code>true</code> if this exit is due to a quit condition.
*/
public static boolean isProcessingQuit()
{
return workArea.obtain().processingQuit;
}
/**
* During global finish processing, this sets the flag for an exit that
* was caused by a <code>QuitConditionException</code>.
*
* @param quit
* <code>true</code> if this exit is due to a quit condition.
*/
public static void setProcessingQuit(boolean quit)
{
workArea.obtain().processingQuit = quit;
}
/**
* Report if this context should be treated as headless. A headless
* session is one that has no user interaction.
*
* @return <code>true</code> if this session is headless.
*/
public static boolean isHeadless()
{
return workArea.obtain().headless;
}
/**
* Increases the number (by 1) of interactive language statements
* (statements which took user input except for PAUSE) that have executed
* in the current block since the most recent start scope, iterate or
* retry.
*/
public static void incrementInteractions()
{
BlockDefinition blk = workArea.obtain().blocks.peek();
blk.interactions++;
}
/**
* Resets the number of interactive language statements (statements which
* took user input except for PAUSE) that have executed in the current
* block to 0.
*/
public static void resetInteractions()
{
BlockDefinition blk = workArea.obtain().blocks.peek();
blk.interactions = 0;
}
/**
* Gets the number of interactive language statements (statements which
* took user input except for PAUSE) that have executed in the current
* block since the most recent start scope, iterate or retry.
*
* @return The number of interactions in the current block.
*/
public static int getInteractions()
{
BlockDefinition blk = workArea.obtain().blocks.peek();
return blk.interactions;
}
/**
* Get the rollup value for the current block. If <code>true</code>,
* then any contained blocks that exit will cause the current (containing)
* block's interactions counter to be increased by the contained block's
* interactions counter. Otherwise, no aggregation will occur.
*
* @return The current block's rollup value.
*/
public static boolean isRollup()
{
BlockDefinition blk = workArea.obtain().blocks.peek();
return blk.rollup;
}
/**
* Get the rollup value for the enclosing block. If <code>true</code>,
* then the current block's exit will cause the enclosing (containing)
* block's interactions counter to be increased by the current block's
* interactions counter. Otherwise, no aggregation will occur.
*
* @return The current block's rollup value.
*/
public static boolean isEnclosingRollup()
{
return isEnclosingRollup(workArea.obtain());
}
/**
* Set the new rollup value for the current block. If <code>true</code>,
* then any contained blocks that exit will cause the current (containing)
* block's interactions counter to be increased by the contained block's
* interactions counter. Otherwise, no aggregation will occur.
*
* @param rollup
* The new rollup value.
*/
public static void setRollup(boolean rollup)
{
BlockDefinition blk = workArea.obtain().blocks.peek();
blk.rollup = rollup;
}
/**
* Notify all registered {@link BatchListener} objects that a batch is
* starting.
*/
public static void batchStart()
{
processBatchNotifications(workArea.obtain(), true);
}
/**
* Notify all registered {@link BatchListener} objects that a batch is
* stopping.
*/
public static void batchStop()
{
processBatchNotifications(workArea.obtain(), false);
}
/**
* Tests the interrupted status of the current thread using
* <code>Thread.interrupted</code>, if <code>true</code> then a
* {@link StopConditionException} will be thrown. The interrupted status
* will be <code>true</code> if a call to <code>Thread.interrupt</code>
* was previously made.
* <p>
* The thread's interrupted status is cleared by this method before the
* <code>StopConditionException</code> is thrown.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
* @throws SilentUnwindException
* If the connection ended prematurely end the current thread
* needs to stop.
*/
public static void honorStopCondition()
throws StopConditionException,
SilentUnwindException
{
workArea.obtain().honorStopCondition();
}
/**
* Method called when the processing needs to be terminated. This will set a flag which will
* indicate that the processing should be ended by raising a {@link
* LeaveUnwindException error}, on the next {@link #honorStopCondition()} call.
*/
public static void abortProcessing()
{
if (workArea.get(false) == null)
{
// if the context does not exist, do not create it at this time
return;
}
WorkArea wa = workArea.obtain();
wa.abortProcessing = true;
}
/**
* Enable/disable nested error mode. This mode allows for storing an error
* which is generated during the processing of a different error, so that
* during {@link #honorError} this can be re-thrown. This duplicates some
* obscure behavior seen in Progress.
*
* @param mode
* <code>true</code> to enable, <code>false</code> to disable.
*/
public static void setNestedMode(boolean mode)
{
workArea.obtain().nested = mode;
}
/**
* Reports on the enable/disable state of nested error mode. This mode
* allows for storing an error which is generated during the processing of
* a different error, so that during {@link #honorError} this can be
* re-thrown.
*
* @return <code>true</code> if nested error mode is enabled.
*/
public static boolean isNestedMode()
{
return workArea.obtain().nested;
}
/**
* Returns the current block type.
*
* @return The type of the current block.
*/
public static BlockType getBlockType()
{
BlockDefinition blk = workArea.obtain().blocks.peek();
return blk.type;
}
/**
* Returns the index of the full transaction block, or -1 if none exists.
*
* @return The index of the full transaction block, or -1 if none exists.
*/
public static int getFullTransactionBlock()
{
return getFullTransactionBlock(workArea.obtain());
}
/**
* Returns the current nesting level.
*
* @return the current nesting level.
*/
public static int getNestingLevel()
{
return workArea.obtain().blocks.size();
}
/**
* Check if this block is the block associated with the root external program,
* in the 4GL legacy stack.
*
* @return See above.
*/
public static boolean isRootNestingLevel()
{
int nestingLevel = getNestingLevel();
WorkArea wa = workArea.obtain();
return nestingLevel == (wa.isRemote() ? 3 : 2);
}
/**
* Check if this block is the block associated with the root external program,
* in the 4GL legacy stack using the nearest external block.
*
* @return See above.
*/
public static boolean isRootExternal()
{
int nestingLevel = findNearestExternal();
WorkArea wa = workArea.obtain();
// in case of appserver calls, the root can be an internal entry or an external program.
// in internal entry case, 'nestingLevel' will be resolved to 2.
return nestingLevel <= (wa.isRemote() ? 3 : 2);
}
/**
* Store the given exception for re-throwing during {@link #honorError} if
* nested error mode is enabled. Note that only 1 error can ever be nested
* and the last one set via this method will be the one that is honored.
*
* @param err
* The error to store.
*/
public static void saveNestedError(ConditionException err)
{
WorkArea wa = workArea.obtain();
if (wa.nested)
{
wa.nestedError = err;
}
}
/**
* Notifies this class that an interactive pause has occurred. This may
* affect the processing of infinite loop protection depending on the
* type of block being processed. This state is set for the remainder of
* the current iteration of the innermost block.
*/
public static void hadPause()
{
// peek at the current block def on the top of the stack
BlockDefinition blk = workArea.obtain().blocks.peek();
blk.hadPause = true;
}
/**
* Search the stack of scopes and return the level of the first external
* block found.
*
* @return The stack level of the nearest enclosing external block or
* -1 if there is no block marked as an external block. Level
* number is 1-based starting from the outermost scope.
*/
public static int findNearestExternal()
{
return findNearestExternal(workArea.obtain());
}
/**
* This method is called before the initialization of a FOR or FOR EACH
* block. It will clear the {@link WorkArea#offEndQueries} set and will
* set the {@link WorkArea#offEndRegistration} flag to ON. While this flag
* is ON, all created queries will be collected in the
* {@link WorkArea#offEndQueries} set.
* <p>
* After the block initialization is finished, the
* {@link #stopOffEndRegistration()} will be called.
*/
public static void startOffEndRegistration()
{
WorkArea wa = workArea.obtain();
wa.offEndRegistration = true;
wa.offEndQueries.push(new HashSet<>());
}
/**
* Pause off-end queries registration started by {@link #startOffEndRegistration()}.
*/
public static void pauseOffEndRegistration()
{
WorkArea wa = workArea.obtain();
wa.savedOffEndRegistration = wa.offEndRegistration;
wa.offEndRegistration = false;
}
/**
* Resume off-end queries registration started by {@link #startOffEndRegistration()} and paused by
* {@link #pauseOffEndRegistration()}.
*/
public static void resumeOffEndRegistration()
{
WorkArea wa = workArea.obtain();
wa.offEndRegistration = wa.savedOffEndRegistration;
wa.savedOffEndRegistration = false;
}
/**
* This method is called after the initialization of a FOR or FOR EACH
* block is finished. It will save all the {@link QueryOffEndListener}s
* associated with the registered {@link WorkArea#offEndQueries queries}.
*/
public static void stopOffEndRegistration()
{
stopOffEndRegistration(workArea.obtain());
}
/**
* Register the passed query for query off-end events. The passed queries
* are all the queries created in the init method for a FOR or FOR EACH
* block.
* <p>
* These queries will be able to end the loop when the last record is
* reached, by overriding any user-defined END-KEY action. The action
* wich is executed is the default UNDO, LEAVE action for END-KEY.
*
* @param query
* The query which needs to be registered.
*/
public static void registerOffEndQuery(P2JQuery query)
{
registerOffEndQuery(workArea.obtain(), query);
}
/**
* Check if the given listener is registered for query off-end events and
* can override the default END-KEY action (which is UNDO, LEAVE), when
* the current block is FOR or FOR EACH.
*
* @param listener
* A listener to be checked if is registered for off-end events.
*
* @return <code>true</code> if the listener is registered for query
* off-end event.
*/
public static boolean isOffEndListener(QueryOffEndListener listener)
{
return isOffEndListener(workArea.obtain(), listener);
}
/**
* Iterate all registered off-end listeners and call the
* {@link QueryOffEndListener#initialize() initialize} or
* {@link QueryOffEndListener#finish() finish} method, depending on the
* flag.
*
* @param init
* <code>true</code> if the listeners are initialized; else, the
* {@link QueryOffEndListener#finish()} method is called.
*/
public static void processOffEndListeners(boolean init)
{
processOffEndListeners(workArea.obtain(), init);
}
/**
* Get the topmost set of off-end {@link QueryOffEndListener listeners}.
*
* @param clear
* <code>true</code> if the top-most off-end listeners set
* should be cleared.
*/
public static Set<QueryOffEndListener> getOffEndListeners(boolean clear)
{
return getOffEndListeners(workArea.obtain(), clear);
}
/**
* Register the single output parameter assigner for the current block.
* This object will be deregistered by the {@link BlockManager} when it is
* time to process assign-backs from function/procedure output parameters
* to the database record fields with which they are associated.
*
* @param opa
* An output parameter assigner.
*
* @throws IllegalStateException
* if a second output parameter assigner is registered for a block
* before the current one is deregistered. There can be only one.
*/
public static void registerOutputParameterAssigner(OutputParameterAssigner opa)
{
registerOutputParameterAssigner(workArea.obtain(), opa);
}
/**
* Defer handling of an exception thrown by a commit/rollback notification
* callback on a registered {@link Commitable}, so that iteration or pop
* scope processing can be continued without interruption. This ensures
* that all <code>Commitable</code>s registered after the errant one still
* receive their commit/rollback notifications, and that (in the case of
* popping a scope), registered {@link Scopeable}s still receive their
* scope finished notifications.
* <p>
* If no error is already pending, <code>exc</code> is stored, to be thrown
* upon conclusion of processing the current block scope or iteration. If
* an error is already pending, <code>exc</code> is discarded. In either
* case, <code>exc</code> is logged if error-level logging is enabled.
*
* @param action
* A brief description of the action occurring at the time the
* error was encountered, such as "commit" or "master rollback".
* @param thr
* The error caught during processing of a commit or rollback
* notification.
*
* @see WorkArea#handleDeferredError
*/
public static void deferError(String action, Throwable thr)
{
deferError(action, thr, false);
}
/**
* Defer handling of an exception thrown by a commit/rollback notification
* callback on a registered {@link Commitable}, so that iteration or pop
* scope processing can be continued without interruption. This ensures
* that all <code>Commitable</code>s registered after the errant one still
* receive their commit/rollback notifications, and that (in the case of
* popping a scope), registered {@link Scopeable}s still receive their
* scope finished notifications.
* <p>
* If no error is already pending, <code>exc</code> is stored, to be thrown
* upon conclusion of processing the current block scope or iteration. If
* an error is already pending, <code>exc</code> is discarded. In either
* case, <code>exc</code> is logged if error-level logging is enabled.
*
* @param action
* A brief description of the action occurring at the time the
* error was encountered, such as "commit" or "master rollback".
* @param thr
* The error caught during processing of a commit or rollback
* notification.
* @param silent
* If <code>true</code> then exception is not logged.
*
* @see WorkArea#handleDeferredError
*/
public static void deferError(String action, Throwable thr, boolean silent)
{
deferError(workArea.obtain(), action, thr, silent);
}
/**
* Search the stack of scopes and return the level of the first top level
* block found.
*
* @return The stack level of the nearest enclosing top level block or
* -1 if there is no block marked as a top level block.
*/
public static int findNearestTopLevel()
{
return findNearestTopLevel(workArea.obtain());
}
/**
* Search the stack of scopes and return the type of first top level block found.
*
* @return The nearest enclosing top level block or {@link BlockType#UNKNOWN} if
* there is no block marked as a top level block.
*/
public static BlockType getNearestTopLevelType()
{
return getNearestTopLevelType(workArea.obtain());
}
/**
* Worker for reporting if this context should be treated as headless. A headless session is
* one that has no user interaction.
*
* <p>
* To determine if the current context is headless the method uses the following algorithm:
* <ul>
* <li>check the {@link SecurityManager#isServerAccount()}. Server sessions are implicit
* headless;
* <li>check the context token named "headless". If it exists, its value tells whether the
* context is for a headless session or not; otherwise the directory is checked;
* <li>check the directory. If there is a {@code boolean} value set to {@code true} for a
* given context's account in one of the following paths, then the context is
* headless. This allows the user interface client to be an optional part of any
* account. In other words, process accounts can be run in a "headless" mode with no
* requirement for any user interface. These are the paths (in order in which they
* are searched):
* <ol>
* <li>{@code /server/<serverID>/runtime/<account_or_group>/headless};
* <li>{@code /server/<serverID>/runtime/default/headless};
* <li>{@code /server/default/runtime/<account_or_group>/headless};
* <li>{@code /server/default/runtime/default/headless};
* </ol>
* <li>otherwise the context is not headless.
* </ul>
*
* @return <code>true</code> if this session is headless.
*/
public static boolean _isHeadless()
{
SecurityManager sm = SecurityManager.getInstance();
if (sm != null && sm.isServerAccount())
{
// in server context we are also in headless mode
return true;
}
SessionManager sessMgr = SessionManager.get();
if (sessMgr != null && sessMgr.isVirtualSession())
{
return true;
}
Boolean headless = (sm != null) ? (Boolean) sm.contextSm.getToken(ContextKey.HEADLESS_KEY) : null;
// second: check directly in the context
if (headless != null && headless)
{
return true;
}
// third: if headless not set in context, check in the directory
else if (headless == null && Utils.getDirectoryNodeBoolean(null, "headless", false))
{
return true;
}
return false;
}
/**
* Register the specified targets as undoables. This will mark the object as undoable and save
* the current tx level, regardless if a tx is active or not.
*
* @param undoTargets
* List of object references to be registered. This cannot be
* <code>null</code>.
*/
public static void registerCurrent(LazyUndoable... undoTargets)
{
registerCurrent(workArea.obtain(), undoTargets);
}
/**
* Worker that notifies all objects in the commit list that they must
* commit (persist any state).
* <p>
* On the last iteration of a loop or block which ends in success (in
* which the flow of control reaches the natural end of the block), both
* will be called in succession. However, other forms of exit which are
* still considered successful (i.e. return and break which are not the
* result of condition processing) will only execute <code>popScope</code>
* and <code>iterate</code> will be bypassed.
* <p>
* It is important to note that the list of commitables in blk is
* iterated in reverse order from that in which they are registered with
* the <code>TransactionManager</code>. This is done to ensure that
* database transactions (which are registered before their corresponding
* buffers) are always committed after all buffers are committed.
*
* @throws ErrorConditionException
* If validation fails.
*/
static void processValidate()
throws ErrorConditionException
{
workArea.obtain().processValidate();
}
/**
* Get the BlockDefinition instance of the nearest block of one of the
* following types:
* <ul>
* <li>{@link BlockType#EXTERNAL_PROC external procedure}</li>
* <li>{@link BlockType#INTERNAL_PROC internal procedure}</li>
* <li>{@link BlockType#FUNCTION user-defined function}</li>
* <li>{@link BlockType#DATABASETRIGGER trigger}</li>
* <li>{@link BlockType#TRIGGER trigger}</li>
* <li>{@link BlockType#EDITING editing}</li>
* </ul>
*
* @return See above.
*/
static BlockDefinition nearestExecutingBlock()
{
return nearestExecutingBlock(workArea.obtain());
}
/**
* Get the executing block at the specified level.
*
* @param levels
* Zero-based depth of the target block, starting from the innermost block.
*
* @return The found block, or <code>null</code> if the index is outside of the block depth.
*/
static BlockDefinition getBlock(int levels)
{
return getBlock(null, levels);
}
/**
* Get the top {@link BlockDefinition}.
*
* @return See above.
*/
static BlockDefinition getBlock()
{
return workArea.obtain().blocks.peek();
}
/**
* Get the executing block at the specified level.
*
* @param wa
* The {@link WorkArea} instance.
* @param levels
* Zero-based depth of the target block, starting from the innermost block.
*
* @return The found block, or <code>null</code> if the index is outside of the block depth.
*/
static BlockDefinition getBlock(WorkArea wa, int levels)
{
if (wa == null)
{
wa = workArea.obtain();
}
if (levels >= wa.blocks.size())
{
return null;
}
return wa.blocks.get(wa.blocks.size() - 1 - levels);
}
/**
* Get the executing block at the specified level.
*
* @param blockDepth
* Zero-based depth of the target block, starting from the outermost block; 0 is the
* global scope.
*
* @return The found block, or <code>null</code> if the index is outside of the block depth.
*/
static BlockDefinition getBlockAtDepth(int blockDepth)
{
return getBlockAtDepth(null, blockDepth);
}
/**
* Get the executing block at the specified level.
*
* @param wa
* The {@link WorkArea} instance.
* @param blockDepth
* Zero-based depth of the target block, starting from the outermost block; 0 is the
* global scope.
*
* @return The found block, or <code>null</code> if the index is outside of the block depth.
*/
static BlockDefinition getBlockAtDepth(WorkArea wa, int blockDepth)
{
if (wa == null)
{
wa = workArea.obtain();
}
if (blockDepth == 0)
{
return wa.globalBlock;
}
if (blockDepth >= wa.blocks.size())
{
return null;
}
return wa.blocks.get(blockDepth);
}
/*
* Deregister the output parameter assigner for the current block, if any.
* It is expected that only the {@link BlockManager} will invoke this
* method.
*
* @return Output parameter assigner for this block, or <code>null</code>
* if none was registered.
*/
public static OutputParameterAssigner deregisterOutputParameterAssigner()
{
return deregisterOutputParameterAssigner(workArea.obtain());
}
/**
* Adds the given object into the list of objects which require undo
* notifications for the next block that is opened.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block. In particular, it is necessary for
* objects that are constructed during a class' constructor/initializer
* (before the external procedure block has opened). Such objects cannot
* simply register since their construction is not actually enclosed by the
* block.
* <p>
* When the deferred registration occurs, it will NOT be global and will
* NOT be added to registered Commitables.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
* <p>
* The given object will only be added if it is not already in the list.
*
* @param target
* The object references to be registered. This cannot be
* <code>null</code>.
*/
static void registerNext(LazyUndoable... target)
{
registerAt(ScopeLevel.NEXT, target);
}
/**
* Adds the given object into the list of objects which require undo
* notifications for the next block that is opened.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block. In particular, it is necessary for
* objects that are constructed during a class' constructor/initializer
* (before the external procedure block has opened). Such objects cannot
* simply register since their construction is not actually enclosed by the
* block.
* <p>
* When the deferred registration occurs, it will NOT be global and will
* NOT be added to registered Commitables.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
* <p>
* The given object will only be added if it is not already in the list.
*
* @param scope
* The scope where to register the undoable targets.
* @param target
* The object references to be registered. This cannot be
* <code>null</code>.
*/
static void registerAt(ScopeLevel scope, LazyUndoable... target)
{
registerAt(workArea.obtain(), scope, target);
}
/**
* Adds the object into a scoped dictionary of objects requiring UNDO support. This registry
* is used to backup the objects at the beginning of transactions or sub-transactions.
* <p>
* These registrations naturally are removed when the associated scope is removed. However,
* the global scope is long-lived. Special care is necessary to prevent the same instance of
* global shared variables being added multiple times.
*
* @param transLevel
* The transaction level to set for this undoable.
* @param persistent
* Flag indicating if the undoable is defined in a persistent procedure
* @param undoTargets
* The object references to be rolled back on failure. This
* cannot be <code>null</code>.
*/
static void registerAt(int transLevel, boolean persistent, LazyUndoable... undoTargets)
{
registerAt(workArea.obtain(), transLevel, persistent, undoTargets);
}
/**
* Deregister the specified undoables from all tx blocks.
*
* @param wa
* The {@link WorkArea} instance; if <code>null</code>, it will be resolved.
* @param undoables
* The undoables to be deregistered.
*/
static void deregister(WorkArea wa, IdentityHashMap<LazyUndoable, Object> undoables)
{
if (wa == null)
{
wa = workArea.obtain();
}
if (wa.transLevel == -1)
{
return;
}
Stack<BlockDefinition> blocks = wa.blocks;
for (int i = 0; i < blocks.size(); i++)
{
BlockDefinition block = blocks.get(i);
if (block.undoables != null)
{
Iterator<UndoablePair> iter = block.undoables.iterator();
while (iter.hasNext())
{
UndoablePair pair = iter.next();
if (undoables.containsKey(pair.src))
{
iter.remove();
}
}
}
if (block.full)
{
break;
}
}
}
/**
* Get the current transaction depth.
*
* @return The {@link WorkArea#txNestingLevel} field.
*/
static int getTransDepth()
{
return workArea.obtain().txNestingLevel;
}
/**
* Enable tracking of all the undaobles defined in the next instantiating external program.
*/
static void trackUndoables()
{
WorkArea wa = workArea.obtain();
wa.trackUndoables = true;
wa.trackedUndoables = new IdentityHashMap<>();
}
/**
* Untrack (by removing from all the tx blocks) all the undoables defined and registered for
* the specified external program.
*
* @param referent
* The external program instance.
*/
static void untrackUndoables(Object referent)
{
untrackUndoables(workArea.obtain(), referent);
}
/**
* Rollback the transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*
* @param chained
* Flag indicating if a new transaction will be started.
*/
static void rollbackTx(boolean chained)
{
rollbackTx(workArea.obtain(), chained);
}
/**
* Commit the transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*
* @param chained
* Flag indicating if a new transaction will be started.
*/
static void commitTx(boolean chained)
{
commitTx(workArea.obtain(), chained);
}
/**
* Begin an explicit transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
* <p>
* This explicitly marks this tx as being open by the
* {@link TransactionResource#getTxInitProcedure() TRANS-INIT-PROCEDURE}.
*/
static void beginTx()
{
beginTx(null, null);
}
/**
* End an explicit transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*/
static void endTx()
{
endTx(null, null);
}
/**
* Logic required when the tx initiated by TRANSACTION-MODE AUTO gets committed or rolled back.
*/
static void notifyMasterFinish()
{
WorkArea wa = workArea.get();
wa.notifyMasterFinish(false);
wa.autoRemoveMasterCommit();
wa.autoRemoveMasterFinish(true);
}
/**
* Remove the specified target from the list of finalizables.
*
* @param finishers
* The list of finalizables.
* @param target
* The target to remove.
*
* @return <code>true</code> if the target was removed.
*/
private static boolean removeFinalizable(Set<Finalizable>[] finishers, Finalizable target)
{
if (finishers == null)
{
return false;
}
Set<Finalizable> l = finishers[target.weight().getIndex()];
return l != null && l.remove(target);
}
/**
* Add the given target to the list of finalizables.
*
* @param block
* The target block.
* @param target
* The target to add.
*/
private static void addFinalizable(BlockDefinition block, Finalizable target)
{
int idx = target.weight().getIndex();
Set<Finalizable> finishers = block.finalizables[idx];
if (finishers == null)
{
finishers = Collections.newSetFromMap(new IdentityHashMap<>(8));
block.finalizables[idx] = finishers;
}
// add the object to the list of finalizable targets if it isn't
// already in the list (avoids being called twice for the same event)
if (finishers.add(target) && target instanceof ConditionListener)
{
if (block.conditionListeners == null)
{
block.conditionListeners = new ArrayList<>(2);
}
block.conditionListeners.add((ConditionListener) target);
}
}
/**
* Register StopAfterTimer to a ContextLocal WorkArea.
*
* @param wa
* WorkArea
* @param stopAfterTimer
* StopAfterTimer to register.
*/
private static void registerStopAfterTimer(WorkArea wa, StopAfterTimer stopAfterTimer)
{
wa.stopAfterTimer = stopAfterTimer;
}
/**
* Adds the given object into the list of objects which require commit notification for the
* block at the specified depth This form of registration is useful for objects whose natural
* lifetime are scoped to a different block than the block in which they must be committed.
* For example, if a global or shared resource needs to be committed in a specific scope,
* then this can be used in cooperation with the normal {@link #registerCurrent} processing to
* control the commit points.
* <p>
* This notification will ONLY occur when there is an active transaction.
* <p>
* Notifications occur in LIFO order; thus, commitables which depend upon
* others being committed first must be registered before the objects upon
* which they are dependent.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param blockDepth
* Zero-based depth of the target block, starting from the outermost block.
* @param target
* The object reference to be committed on success. This cannot be
* <code>null</code>.
*/
private static void registerCommitAt(WorkArea wa, int blockDepth, Commitable target)
{
BlockDefinition blk = wa.blocks.get(blockDepth);
// prevent ConcurrentModificationException
if (blk.commitables != null && wa.nowIterating == blk.commitables)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"registerCommit",
"Illegal attempt to register Commitable",
new Throwable());
}
return;
}
if (blk.commitables == null)
{
blk.commitables = new ArrayList<>();
}
// add the object to the list of commit targets
blk.commitables.add(target);
}
/**
* Registers the instance for scope notifications at the current block.
* <p>
* This will call {@link Scopeable#scopeStart} to notify for the scope processing.
*
* @param wa
* The {@link WorkArea} instance.
* @param target
* The scopeable instance.
*/
private static void registerBlockScopeable(WorkArea wa, Scopeable target)
{
BlockDefinition block = wa.blocks.peek();
registerScopeable(wa, block, target, true);
}
/**
* Register the target scopeable as pending, to be added to the next block.
*
* @param wa
* The {@link WorkArea} instance.
* @param target
* The scopeable instance.
*/
private static void registerPendingScopeable(WorkArea wa, Scopeable target)
{
if (wa.nowIterating != null && wa.nowIterating == wa.blocks.peek().scopeList)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"registerPendingScopeable",
"Illegal attempt to register Scopeable",
new Throwable());
}
return;
}
if (wa.pendingScopeables == null)
{
wa.pendingScopeables = new Scopeable[ScopeId.values().length];
}
wa.pendingScopeables[target.getScopeId().ordinal()] = target;
}
/**
* Registers the instance for scope notifications at the current block.
*
* @param wa
* The {@link WorkArea} instance.
* @param block
* The target block.
* @param target
* The scopeable instance.
* @param start
* Flag indicating if {@link Scopeable#scopeStart} should be executed.
*/
private static void registerScopeable(WorkArea wa, BlockDefinition block, Scopeable target, boolean start)
{
int id = target.getScopeId().ordinal();
if (wa.nowIterating != null && wa.nowIterating == block.scopeList && block.scopeList[id] == null)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"registerScopeable",
"Illegal attempt to register Scopeable",
new Throwable());
}
return;
}
if (block.scopeList == null)
{
block.scopeList = new Scopeable[ScopeId.values().length];
}
if (block.scopeList[id] == null)
{
block.scopeList[id] = target;
if (start)
{
// process the scope start
target.scopeStart(block);
}
}
}
/**
* Remove the given finalizable from the global list of finalizables.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param target
* Finalizable to be removed from the global list.
*/
private static void deregisterGlobalFinalizable(WorkArea wa, Finalizable target)
{
// prevent ConcurrentModificationException
if (wa.nowIterating == wa.globalBlock.finalizables)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"deregisterGlobalFinalizable",
"Illegal attempt to deregister Finalizable",
new Throwable());
}
return;
}
removeFinalizable(wa.globalBlock.finalizables, target);
}
/**
* Adds the given object into the list of objects for the specified block
* which require iterate/retry/finish notification. This form of
* registration is useful for objects whose natural lifetime is scoped to
* a block and which need a hook for cleaning up resources or handling
* other block termination processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called)
* in the case of a non-global scope OR at the moment that the last
* scope has been removed in the case of the global scope.
* <p>
* The given object will only be added if it is not already in the list.
* <p>
* Finalizables for a scope are processed in the same order as they were
* registered.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param blockDepth
* zero-based depth of target block, starting from the outermost
* block. <code>0</code> indicates the global block.
* @param target
* The object reference to be notified on any exit from the
* block specified. This cannot be <code>null</code>.
*/
private static void registerFinalizableAt(WorkArea wa, int blockDepth, Finalizable target)
{
BlockDefinition targetBlock;
if (blockDepth == 0)
{
// 0 indicates globalBlock rather than block with the index 0 (startup block).
targetBlock = wa.globalBlock;
}
else
{
BlockDefinition blk = wa.blocks.get(blockDepth);
targetBlock = blk;
}
// prevent ConcurrentModificationException
if (wa.nowIterating == targetBlock.finalizables)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"registerFinalizable",
"Illegal attempt to register Finalizable",
new Throwable());
}
return;
}
addFinalizable(targetBlock, target);
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notifications for the next block that is opened
* which is a top-level block that corresponds to a Progress external
* procedure.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block and which need a hook for cleaning up
* resources or handling other block termination processing. In
* particular, it is necessary for objects that are constructed during
* a class' constructor/initializer (before the external procedure block
* has opened). Such objects cannot simply register since their
* construction is not actually enclosed by the block.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param target
* The object reference to be notified on any exit from the
* top-level external block. This cannot be <code>null</code>.
*
* @throws IllegalStateException
* if the top-level block cannot be identified.
*/
private static void registerNextExternal(WorkArea wa, Finalizable target)
{
if (wa.deferredExtFinalizables == null)
{
wa.deferredExtFinalizables = new ArrayList<>();
}
// prevent ConcurrentModificationException
if (wa.nowIterating == wa.deferredExtFinalizables)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"registerNextExternal",
"Illegal attempt to register Finalizable",
new Throwable());
}
return;
}
// add the object to the list of finalizable targets if it isn't
// already in the list (avoids being called twice for the same event)
if (!wa.deferredExtFinalizables.contains(target))
{
wa.deferredExtFinalizables.add(target);
}
}
/**
* Allow finalizables to do their cleanup ({@link Finalizable#initFailure()}) now since the
* scoped they were designed was aborted and there won't be other chance to call their
* {@link Finalizable#finished()}.
* <p>
* After iteration the list is deleted.
* <p>
* Also, cleanup all other pending state being collected while the external program was
* instantiating.
*
* @param wa
* Context local work area. Must not be {@code null}.
*/
private static void cleanupPending(WorkArea wa)
{
if (wa.deferredExtFinalizables != null)
{
wa.deferredExtFinalizables.forEach(Finalizable::initFailure);
wa.deferredExtFinalizables = null;
}
wa.deferredUndoables = null;
wa.trackedUndoables = null;
wa.trackUndoables = false;
wa.deferredDynExt = null;
wa.pendingScopeables = null;
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notifications for the nearest enclosing
* top-level block. A top-level block is generated for Progress external
* procedures, internal procedures, triggers and functions. An additional
* parameter allows registration to be specified for the specific block
* which corresponds with an external procedure block scope in Progress.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block and which need a hook for cleaning up
* resources or handling other block termination processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
* <p>
* Finalizables for a scope are processed in the same order as they were
* registered.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param target
* The object reference to be notified on any exit from the
* top-level block. This cannot be <code>null</code>.
* @param external
* If <code>true</code>, register with the nearest enclosing
* external block (external procedure), otherwise register with
* the nearest enclosing top-level block (which may be but is
* not guaranteed to be the external block).
*
* @throws IllegalStateException
* if the top-level block cannot be identified.
*/
private static void registerTopLevelFinalizable(WorkArea wa,
Finalizable target,
boolean external)
{
BlockDefinition targetBlock = null;
Stack<BlockDefinition> blocks = wa.blocks;
ListIterator<BlockDefinition> iter = blocks.listIterator(blocks.size());
// walk backwards up the stack of block definitions
while (iter.hasPrevious())
{
BlockDefinition blk = iter.previous();
// if we are looking for an external block and found one OR
// if we are NOT looking for an external block and found any top
// level block
if ((external && blk.external) || (!external && blk.topLevel))
{
// done!
targetBlock = blk;
break;
}
}
if (targetBlock == null)
{
String errmsg = (external ? "external" : "top-level");
throw new IllegalStateException("Unable to find " + errmsg + " block");
}
// prevent ConcurrentModificationException
if (wa.nowIterating == targetBlock.finalizables)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"registerTopLevelFinalizable",
"Illegal attempt to register Finalizable",
new Throwable());
}
return;
}
addFinalizable(targetBlock, target);
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notification for the nearest enclosing, looping
* block.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the nearest enclosing, looping block and which need a hook
* for cleaning up resources or handling other block termination
* processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param target
* The object reference to be notified on any exit from or
* iteration of the target block. This cannot be <code>null</code>.
*
* @throws IllegalStateException
* if no enclosing, looping block can be identified.
*/
private static void registerNearestLoopFinalizable(WorkArea wa, Finalizable target)
{
// walk backwards up the stack of block definitions
Stack<BlockDefinition> blocks = wa.blocks;
ListIterator<BlockDefinition> iter = blocks.listIterator(blocks.size());
while (iter.hasPrevious())
{
BlockDefinition blk = iter.previous();
if (blk.loop)
{
// prevent ConcurrentModificationException
if (wa.nowIterating == blk.finalizables)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"registerNearestLoopFinalizable",
"Illegal attempt to register Finalizable",
new Throwable());
}
return;
}
addFinalizable(blk, target);
return;
}
}
throw new IllegalStateException("Unable to find nearest enclosing, looping block");
}
/**
* Removes the given object from the list of objects which require
* iterate/retry/finish notifications. This will find the first instance
* of the given reference by walking up each block from the current to the
* global block. The first instance found will be removed and the method
* will then return. If the same instance is registered in multiple scopes,
* it would take multiple calls to remove all instances.
* <p>
* After this call, no notifications will be received.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param target
* The object reference to be removed. Must not be {@code null}.
*
* @return <code>true</code> if the target instance was found and
* removed.
*/
private static boolean removeFinalizable(WorkArea wa, Finalizable target)
{
Stack<BlockDefinition> blocks = wa.blocks;
ListIterator<BlockDefinition> iter = blocks.listIterator(blocks.size());
boolean found = false;
// walk the stack of block definitions from the current back "up"
while (iter.hasPrevious() && !found)
{
BlockDefinition blk = iter.previous();
if (wa.nowIterating != blk.finalizables)
{
found = removeFinalizable(blk.finalizables, target);
}
else
{
// prevent ConcurrentModificationException
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"removeFinalizable",
"Illegal attempt to deregister Finalizable",
new Throwable());
}
return false;
}
}
// if still not found, try the global block
if (!found)
{
if (wa.nowIterating != wa.globalBlock.finalizables)
{
found = removeFinalizable(wa.globalBlock.finalizables, target);
}
else
{
// prevent ConcurrentModificationException
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"removeFinalizable",
"Illegal attempt to deregister global Finalizable",
new Throwable());
}
return false;
}
}
return found;
}
/**
* Reports whether the current block was just rolled back or a containing
* block has a pending rollback. If there is no current transaction open,
* then there cannot be a pending or current rollback.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return <code>true</code> if there was or will be a rollback.
*/
private static boolean hadRollback(WorkArea wa)
{
// if there is no transaction, there can't be a pending rollback or a
// current rollback
if (wa.transLevel != -1)
{
// check if a containing block has a pending rollback
if (wa.rollbackPending)
{
return true;
}
BlockDefinition blk = wa.blocks.peek();
// check if the current block was rolled back
if (blk.rolledBack)
{
return true;
}
}
return false;
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the scope defined
* explicitly by the given label or implicitly if the label is
* <code>null</code>. All objects that are rolled back will have their
* values assigned based on the values backed up at the start of the
* specified scope. If the current scope is not the target scope, the
* given exception will be stored, the rollback will be deferred and
* the rollback will be processed as the scopes naturally unwind. This
* unwinding must be generated by a change in flow of control by the
* calling code. This flow control change MUST always immediately follow
* this method otherwise undefined results can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param label
* Specifies the name of the scope at which to rollback or
* <code>null</code> if the scope should be implicitly
* determined.
* @param cause
* The exceptional cause, if any, of the rollback.
*/
private static void rollback(WorkArea wa, String label, Throwable cause)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "rollback", "rollback_label = " + label);
}
// find the target block and call our worker
int level = findStackLevel(wa, label);
rollbackWorker(wa, level, cause);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the nearest top level scope.
* All objects that are rolled back will have their values assigned based
* on the values backed up at the start of the specified scope. If the
* current scope is not the target scope, the given exception will be
* stored, the rollback will be deferred and the rollback will be processed
* as the scopes naturally unwind. This unwinding must be generated by a
* change in flow of control by the calling code. This flow control change
* MUST always immediately follow this method otherwise undefined results
* can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*
* @param wa
* Context local work area. Must not be {@code null}.
*/
private static void rollbackTopLevel(WorkArea wa)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "rollback nearest top level block", null);
}
// find the target block and call the worker
int level = findNearestTopLevel(wa);
rollbackWorker(wa, level, null);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the scope defined by the
* specified stack level. All objects that are rolled back will have their
* values assigned based on the values backed up at the start of the
* specified scope. If the current scope is not the target scope, the
* given exception will be stored, the rollback will be deferred and
* the rollback will be processed as the scopes naturally unwind. This
* unwinding must be generated by a change in flow of control by the
* calling code. This flow control change MUST always immediately follow
* this method otherwise undefined results can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param level
* Specifies the stack level of the scope at which to rollback or
* -1 if the current scope should be the target.
* @param cause
* The exceptional cause, if any, of the rollback.
*/
private static void rollbackWorker(WorkArea wa, int level, Throwable cause)
{
BlockDefinition blk = null;
if (level == -1)
{
blk = wa.blocks.peek();
}
else
{
blk = wa.blocks.elementAt(level);
}
// mark that block as being the UNDO target
blk.isUndoLevel = true;
// rollback an only occur inside an active transaction
if (wa.isTransaction())
{
// is there a pending rollback?
if (wa.isRollbackPending())
{
// this should not happen!
String errmsg = "There is already a pending rollback.";
if (wa.rollbackPendingCause != null)
{
LOG.log(Level.SEVERE, errmsg + " Pending cause:", wa.rollbackPendingCause);
}
else
{
LOG.severe(errmsg + " Pending cause unavailable.");
}
if (cause != null)
{
LOG.log(Level.SEVERE, errmsg + " Current cause:", cause);
}
else
{
LOG.severe(errmsg + " Current cause unavailable.");
}
throw new IllegalStateException(errmsg);
}
else
{
// determine if an explicit label refers to this scope OR if
// a lack of a label implicitly refers to this scope
if (rollbackCurrentScope(wa, level))
{
// peek at the current block def on the top of the stack
if (level != -1)
{
blk = wa.blocks.peek();
}
// remember that we rolled back so on exit there is no commit
blk.rolledBack = true;
blk.undoThrow = true;
// we will immediately roll back, which allows other subsequent
// processing like retry to work properly
processRollback(wa, blk);
}
else
{
// the undo is destined for an enclosing scope, defer it; this
// must be done even if the enclosing target scope is outside
// of the transaction since otherwise, when popScope() exits
// the transaction it will not know that there is a pending
// rollback and the full transaction will be committed... that
// would be a bad thing
wa.rollbackScope = level;
wa.rollbackPending = true;
}
processResettables(wa, true);
}
}
else
{
// no transaction but we still need to remember that this occurred
// so that any following retry will know the target block for an
// implicit retry (a null label in triggerRetry/forceRetry) - note
// that we may be setting the target scope to null right here if the
// label parm to this method is null, that is OK
wa.retryScope = level;
processResettables(wa, false);
}
}
/**
* Initializes a new scope, the associated block definition data
* structures. A backup of objects that can be rolled back is deferred
* until after the block opens (see {@link #blockSetup} and
* {@link #makeBackup}).
* <p>
* In the case of a block which is defined as <code>NO_TRANSACTION</code>,
* the core data structures are initialized but no rollback support will
* be provided.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param label
* The text label associated with the block. This usually
* should correspond with the label used in the source file
* to identify the block for purposes of the break and continue
* statements.
* @param level
* One of {@link #NO_TRANSACTION}, {@link #SUB_TRANSACTION} or
* {@link #TRANSACTION}. This code defines the level of
* rollback and transaction support which should be provided.
* @param props
* A bitfield storing block properties which will be a
* combination of {@link #PROP_NONE}, {@link #PROP_ERROR},
* {@link #PROP_ENDKEY}, {@link #PROP_STOP} or
* {@link #PROP_QUIT}.
* @param external
* <code>true</code> if this block is associated with the
* external procedure (in Progress terms). This controls the
* cleanup of certain resources such as temp-tables, frames
* and streams.
* @param topLevel
* <code>true</code> if this block is a method.
* @param loop
* Defines if this block is a loop or not.
* @param next
* <code>true</code> if this infinite loop protection should
* convert a retry into a next (continue), otherwise the retry
* will be converted into a leave (break).
* @param databaseTrigger
* <code>true</code> if this scope should define a
* database trigger block
* @param blockType
* Specifies this block's type.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
private static void pushScope(WorkArea wa,
String label,
int level,
int props,
boolean external,
boolean topLevel,
boolean loop,
boolean next,
boolean databaseTrigger,
BlockType blockType)
{
if (LOG.isLoggable(Level.FINE))
{
String spec = "new block: label = %s; type = %s; level = %s; " +
"external = %b; top_level = %b; loop = %b; " +
"next_or_leave = %s; properties = '%s';";
Object[] parms = new Object[]
{
label,
blockType.toString(),
decodeLevel(level),
external,
topLevel,
loop,
(next ? "next" : "leave"),
decodeProperties(props)
};
log(Level.FINE, "pushScope", String.format(spec, parms));
}
try
{
// check the interrupted status and throw the stop condition if needed
wa.honorStopCondition();
}
catch (RuntimeException exe)
{
ControlFlowOps.cleanupPending();
throw exe;
}
// remember if this scope starts a new transaction rather than a
// sub-transaction or no transaction
boolean full = false;
// start a new transaction if needed and one is not already active
if (isTransactionLevel(level) && !wa.isTransaction())
{
full = true;
}
// determine if we have to mark this block for FOR block aggressive buffer flushing: if the
// new block is any type of FOR block, turn on the forFlushing property
boolean forFlushing = false;
if (!external)
{
// any FOR block enables forFlushing state
switch (blockType)
{
case FOR_LOOP:
case FOR_LOOP_WHILE:
case FOR_LOOP_TO:
case FOR_LOOP_TO_WHILE:
case FOR_BLOCK:
case FOR_BLOCK_WHILE:
case FOR_BLOCK_TO:
case FOR_BLOCK_TO_WHILE:
forFlushing = true;
default:
break;
}
}
// create the block definition
BlockDefinition blk = new BlockDefinition(label,
level,
props,
full,
external,
topLevel,
loop,
next,
forFlushing,
databaseTrigger,
blockType);
if ((props & PROP_ENDKEY) != 0 && wa.lt != null && wa.isInteractive()) // wa.lt is NULL for headless mode
{
// LT must not be added for agents or batch mode
// this is registered directly at the block, to avoid using the 'pending'
// WARNING: LT has dependencies on top-level blocks (including external); currently, such blocks have
// by default the ENDKEY property - if this registration changes, attention is needed to register
// LT scopeable for these blocks.
registerScopeable(wa, blk, wa.lt, false);
}
// add our block
wa.blocks.push(blk);
// add a new query off-end listeners set to the stack
wa.offEndListeners.push(Collections.emptySet());
// mark our global state as having an active transaction if needed
if (full)
{
// this must be after we have added the latest block to our stack
// in order to properly have a 0-based index
wa.setTransactionLevel(wa.blocks.size() - 1);
}
if (wa.processUndoables())
{
wa.txNestingLevel++;
blk.txNestingLevel = wa.txNestingLevel;
}
// the error manager needs a special notification (it can't be a
// scopeable because it is used on both the client and the server)
if (topLevel)
{
// remember if silent mode was enabled or not
blk.suppressError = wa.em.isSilent();
// disable silent mode if it was enabled because silent mode on a
// RUN statement or for a function call is something that only
// suppresses errors during the dispatching itself and NOT during the
// execution of the called code
if (blk.suppressError)
{
wa.em.setSilent(false);
}
}
if (wa.bm != null)
{
if (wa.bm.isImportantBlockTransition())
{
BufferManager.registerTxScopeable(wa.bm);
}
if (isGlobalBlock(wa) || blk.external)
{
// the global block must be added, for the scoped dictionary's global block.
// all external blocks must register also
BufferManager.registerPendingScopeable(wa.bm);
}
}
if (blk.external)
{
ArrayAssigner.registerPendingScopeable();
}
// notify Scopeables that scope has opened
processScopeNotifications(wa, blk, true);
// process deferred external procedure block registration requests
if (external && wa.deferredExtFinalizables != null)
{
try
{
wa.nowIterating = wa.deferredExtFinalizables;
ArrayList<Finalizable> deferredExtFinalizables = wa.deferredExtFinalizables;
for (int i = 0; i < deferredExtFinalizables.size(); i++)
{
Finalizable target = deferredExtFinalizables.get(i);
registerTopLevelFinalizable(target, true);
}
// clear the list
wa.deferredExtFinalizables = null;
}
finally
{
wa.nowIterating = null;
}
}
// process deferred block registration requests
if (wa.deferredUndoables != null)
{
try
{
boolean persistent = (blk.external && wa.trackUndoables);
wa.nowIterating = wa.deferredUndoables;
for (LazyUndoable target : wa.deferredUndoables.keySet())
{
registerUndo(target, wa.txNestingLevel, persistent);
}
// clear the list
wa.deferredUndoables = null;
}
finally
{
wa.nowIterating = null;
}
}
if (wa.deferredDynExt != null)
{
try
{
// uninitialized dynamic extent vars
ArrayAssigner.registerDynamicUndoableArray(wa.deferredDynExt, wa.txNestingLevel);
}
finally
{
wa.deferredDynExt = null;
}
}
if (blk.external && wa.trackUndoables)
{
// if this is the scope for an external program and undoables are being tracked
// (i.e. the program is ran persistent), then save all the defined undoables, so that
// they can be removed from all tx blocks, when the persistent proc gets deleted.
Object referent = wa.pm._thisProcedure();
if (wa.trackedUndoables != null && wa.trackedUndoables.isEmpty())
{
wa.procUndoables.put(referent, wa.trackedUndoables);
}
wa.trackUndoables = false;
wa.trackedUndoables = null;
}
}
/**
* Remove the current block from the stack of scopes, process any pending
* rollbacks (if the current block is the proper target), notify any
* objects needing commits (if the block finished successfully) and handle
* finish processing for the block. This method must be called from a
* <code>finally</code> block such that this is guaranteed to be called
* no matter how the flow of control exits the associated block (exception,
* break, return or the flow of control naturally passing through the
* end of the block).
* <p>
* Rollback and commit processing only occur when there is an active
* transaction. Otherwise this processing is bypassed. Finish processing
* is always called.
* <p>
* This method implements global finish processing (as the last action
* before returning) when the last scope is popped from the stack of
* scopes. For this reason, the current implementation assumes that the
* session is over at the moment that the top-most user-defined scope
* exits.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
private static void popScope(WorkArea wa)
throws StopConditionException
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "popScope", null);
}
// get the top block def on the stack
BlockDefinition blk = wa.blocks.peek();
if (blk.topLevel &&
(blk.scopeList == null || blk.scopeList[ScopeId.BUFFER_MANAGER.ordinal()] == null) &&
wa.pm._isDelayedDelete(wa.pm._thisProcedure()))
{
BufferManager.registerScopeable(wa.bm);
}
try
{
if (wa.processUndoables())
{
wa.txNestingLevel--;
}
// if we have no more scopes in the stack of scopes, set flag for global termination
// this global termination is the same for both appserver agents and normal clients - the state kept
// in the global block needs to be finalized only when the actual client gets terminated.
boolean globalTerm = (wa.blocks.size() == 1);
// transaction related processing
if (wa.isTransaction())
{
// is there a pending rollback?
if (wa.isRollbackPending())
{
// determine if a pending rollback refers to this scope
if (rollbackCurrentScope(wa, wa.getRollbackScope()))
{
processRollback(wa, blk);
// temporary mark block as rolled back to allow locks downgrade
blk.rolledBack = true;
}
// previously, if 'rollbackCurrentScope' was false, a 'processRollbackPending' method was being
// called, which was executing the 'rollbackPending' for each Commitable. But as there was
// no real implementation of this method anywhere in FWD, this was removed.
}
else
{
// we only commit if we haven't rolled back in this scope
if (!blk.rolledBack)
{
// OK, this is a case where we need to COMMIT!
processCommit(wa, blk);
}
}
}
// prevent downstream code from retrying
if (wa.retryScope != -1 && wa.retryScope == (wa.blocks.size() - 1))
{
wa.retryScope = -1;
}
// handle cleanup notifications
processFinalizables(wa, blk, false);
try
{
try
{
try
{
if (blk.possibleCatch != null)
{
try
{
executeBlockTermination(wa, blk,
() -> processTerminationBlock(wa, blk, blk.possibleCatch), false);
}
finally
{
blk.possibleCatch = null;
}
}
}
finally
{
if (blk.finallyBlock != null)
{
try
{
// FINALLY block support (this MUST be AFTER commit/rollback and finalizables are
// processed but BEFORE the scope notifications)
executeBlockTermination(wa, blk,
() -> processTerminationBlock(wa, blk, blk.finallyBlock), true);
}
finally
{
blk.finallyBlock = null;
}
}
wa.honorStopAfterCondition(blk);
}
}
finally
{
if (!wa.isRemote() && isGlobalBlock(wa))
{
// this will be called after the termination of the root top-level block for standalone
// clients or appserver agents (the root top-level block is always a child of the global
// block, so this termination hook can be considered a FINALLY block for the global block).
// FWD ensures that this code is executed after all normal application logic has executed
// (including any FINALLY block processing for the root top-level block). As this is
// considered a FINALLY code for the global block, we need to execute in the same terms as
// normal FINALLY blocks (AFTER commit/rollback and finalizables are processed
// but BEFORE the scope notifications)
processTerminationHook(wa);
}
}
}
finally
{
// now we are done with the scope
processScopeNotifications(wa, blk, false);
// the error manager needs a special notification (it can't be a
// scopeable because it is used on both the client and the server)
if (blk.topLevel && blk.suppressError)
{
// restore silent mode
wa.em.setSilent(true);
}
if (blk.undoables != null)
{
ArrayList<UndoablePair> undoables = blk.undoables;
for (int i = 0; i < undoables.size(); i++)
{
UndoablePair target = undoables.get(i);
target.src.popBlock();
}
}
// top level blocks must clear our transaction flag
if (wa.isTransactionBlock())
{
wa.setTransactionLevel(-1);
}
// global termination occurs as the last thing after the last scope is removed
if (globalTerm)
{
processFinalizables(wa, wa.globalBlock, false);
}
// auto-remove processing for master commitables and finalizables
if (wa.isFullTransaction())
{
wa.autoRemoveMasterCommit();
wa.autoRemoveMasterFinish();
}
}
}
finally
{
// pop the top block definition
wa.blocks.pop();
// pop the query off-end listeners stack
wa.offEndListeners.pop();
if (blk != null)
{
// propagate any disabling of loop protection in the inner block back to any
// enclosing inner block BUT NOT past the top level block
if (!blk.topLevel && wa.blocks.size() > 0)
{
// save off the exiting block's loop protection values
boolean prot = blk.loopProtection;
boolean pause = blk.hadPause;
boolean endkey = blk.endkeyRetry;
BlockDefinition enclosing = null;
// did the inner block have loop protection disabled
if (!prot)
{
enclosing = wa.blocks.peek();
enclosing.loopProtection = false;
}
// did the inner block have a pause occur
if (pause)
{
if (enclosing == null)
{
enclosing = wa.blocks.peek();
}
enclosing.hadPause = true;
}
if (endkey)
{
if (enclosing == null)
{
enclosing = wa.blocks.peek();
}
enclosing.endkeyRetry = true;
}
if (blk.interactions > 0)
{
if (enclosing == null)
{
enclosing = wa.blocks.peek();
}
if (enclosing.rollup)
{
enclosing.interactions += blk.interactions;
}
}
}
}
}
wa.handleDeferredError();
}
/**
* Execute legacy block termination blocks like CATCH and FINALLY.
* <p>
* This will save and restore the {@link WorkArea#masterCommit} and {@link WorkArea#masterFinish} maps.
*
* @param wa
* The {@link WorkArea} instance.
* @param blk
* The parent block definition.
* @param code
* The code to execute.
* @param isFinally
* Flag indicating a FINALLY block is executing.
*/
private static void executeBlockTermination(WorkArea wa,
BlockDefinition blk,
Runnable code,
boolean isFinally)
{
Map<Object, Boolean> savedMasterCommit = null;
Map<Object, Boolean> savedMasterFinish = null;
// temporarily suspend/hide master commitables and finalizables while the code is being executed
if (wa.masterCommit != null && !wa.masterCommit.isEmpty())
{
savedMasterCommit = wa.masterCommit;
wa.masterCommit = null;
}
if (wa.masterFinish != null && !wa.masterFinish.isEmpty())
{
savedMasterFinish = wa.masterFinish;
wa.masterFinish = null;
}
boolean nestedSilent = (isFinally && blk.wasReturnError);
try
{
if (nestedSilent)
{
ErrorManager.nestedSilent(code);
}
else
{
code.run();
}
}
catch (ReturnUnwindException rue)
{
switch (blk.type)
{
case FUNCTION:
case EXTERNAL_PROC:
case INTERNAL_PROC:
case DATABASETRIGGER:
break; // ignore it, returning anyway
default:
throw rue; // rethrow in order to return from the top-level routine (function/procedure)
}
}
finally
{
// restore the master commitables and finalizables, if they were stacked and hidden
if (savedMasterCommit != null)
{
wa.masterCommit = savedMasterCommit;
savedMasterCommit = null;
}
if (savedMasterFinish != null)
{
wa.masterFinish = savedMasterFinish;
savedMasterFinish = null;
}
}
}
/**
* When the {@link #isGlobalBlock() the root top-level block} terminates, invoke any hook specified by
* the {@link CommonSession#isClientDisconnected() SESSION:CLIENT-DISCONNECTED attribute}.
*
* @param wa
* The context local state.
*/
private static void processTerminationHook(WorkArea wa)
{
character hook = SessionUtils.getTerminationHook();
if (hook.isUnknown() || hook.toJavaType().trim().isEmpty())
{
return;
}
ErrorManager.silent(() ->
{
String target = hook.toJavaType();
try
{
int colon = target.indexOf(':');
if (colon > 0 && target.lastIndexOf(':') == colon)
{
// the com.goldencode.Foo:cleanup syntax, a static method call
String cls = target.substring(0, colon);
String mthd = target.substring(colon + 1);
ObjectOps.invokeStandalone(cls, mthd);
}
else
{
// an external program
ControlFlowOps.invokeExternalProcedure(hook, false, null, false, null, null);
}
}
catch (Throwable t)
{
// ignore any error, but log it
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE, "Abnormal end when processing termination hook '" + target + "':", t);
}
}
});
}
/**
* Execute the FINALLY block associated with the block definition, if any.
* <p>
* The FINALLY block will always be executed for the associated block, even if the stack unwinds because
* of an abend. WARNING: it is important to not change this behavior, FWD must allow the FINALLY code to
* execute even if in cases of abends. It is the application code's responsibility to ensure that the
* code being executed is server-side only, maybe protected via the
* {@link CommonSession#isClientDisconnected() SESSION:CLIENT-DISCONNECTED attribute}.
*
* @param wa
* The context local state.
* @param blk
* The block definition to process.
*/
private static void processTerminationBlock(WorkArea wa, BlockDefinition blk, Runnable code)
{
int transLevel = -1;
int workTxNestingLevel = -1;
int blkTxNestingLevel = -1;
if (code != null)
{
// clear them, DeferredDeletablesManager can get registered again in the FINALLY block; but restore
// them once that finishes.
Set<Finalizable>[] prev = blk.finalizables;
blk.finalizables = new Set[WeightFactor.WEIGHTS_ARRAY_SIZE];
try
{
if (blk.full)
{
transLevel = wa.transLevel;
workTxNestingLevel = wa.txNestingLevel;
blkTxNestingLevel = blk.txNestingLevel;
wa.transLevel = -1;
wa.txNestingLevel = -1;
blk.txNestingLevel = -1;
}
code.run();
}
catch (ReturnUnwindException rue)
{
switch (blk.type)
{
case FUNCTION:
case EXTERNAL_PROC:
case INTERNAL_PROC:
case DATABASETRIGGER:
break; // ignore it, returning anyway
default:
throw rue; // rethrow in order to return from the top-level routine (function/procedure)
}
}
finally
{
if (blk.full)
{
wa.transLevel = transLevel;
wa.txNestingLevel = workTxNestingLevel;
blk.txNestingLevel = blkTxNestingLevel;
}
// execute again block's finalizables, if new ones were registered
processFinalizables(wa, blk, false);
// add back the previous ones
blk.finalizables = prev;
}
}
}
/**
* Notify {@link ConditionListener} instances about exception condition.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param condition
* The instance of {@link ConditionException}.
*/
private static void notifyCondition(WorkArea wa, ConditionException condition)
{
notifyCondition(wa, BlockManager.exceptionToCondition(condition));
}
/**
* Notify {@link ConditionListener} instances about condition.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param condition
* The condition the listeners will be notified.
*/
private static void notifyCondition(WorkArea wa, BlockManager.Condition condition)
{
BlockDefinition blk = wa.blocks.peek();
if (blk.conditionListeners != null)
{
ArrayList<ConditionListener> conditionListeners = blk.conditionListeners;
for (int i = 0; i < conditionListeners.size(); i++)
{
ConditionListener listener = conditionListeners.get(i);
try
{
listener.condition(condition);
}
catch (Throwable thr)
{
deferError(wa, "notifyCondition", thr, false);
}
}
}
}
/**
* Provides a callback entry point for the top of the loop. This location
* is required to handle the backup of undo variables via
* {@link #makeBackup}. On the second and subsequent executions of this
* method for a given block, this method handles <code>iterate</code>
* notifications.
* <p>
* It is important to note that <code>continue</code> can be called from
* nested blocks at arbitrary depths. This method can handle such nesting
* at any level.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
private static void blockSetup(WorkArea wa)
throws StopConditionException
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "blockSetup", null);
}
// read the block def at the top of the stack
BlockDefinition blk = wa.blocks.peek();
if (blk.init)
{
// second or subsequent time through
iterateWorker(wa);
blk.iterated = true;
processResettables(wa, true);
}
else
{
// first time through
processEntry(wa, blk);
blk.init = true;
}
// now trigger the undoable backups
backupWorker(wa, blk);
}
/**
* Reports if the current iteration of this block or loop is a retry.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return <code>true</code> if this is a retry of this block or loop
* (as opposed to the first time through this iteration).
*/
private static boolean _isRetry(WorkArea wa)
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
disableLoopProtection(blk);
return blk.wasRetried;
}
/**
* Records the fact that the pending RETRY is being caused by an ENDKEY
* condition. This enables "downstream" retry processing to implement the
* "stuttering" quirk and some behavior in honoring PAUSE as a way to
* disable infinite loop protection.
*
* @param wa
* Context local work area. Must not be {@code null}.
*/
private static void markEndkeyRetry(WorkArea wa)
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
blk.endkeyRetry = true;
}
/**
* This checks if any of the enclosing loops has iterated at least once.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return <code>true</code> if any of the enclosing loops has iterated
* at least once.
*/
private static boolean hasParentIteratedBlock(WorkArea wa)
{
for (BlockDefinition blk : wa.blocks)
{
if (blk.loop && blk.iterated)
{
return true;
}
}
return false;
}
/**
* Determines if a NEXT operation with the given target block should
* be honored or if the NEXT should be converted to a LEAVE. This is
* a special form of infinite loop protection.
* <p>
* A NEXT is converted into a LEAVE when all of the following conditions
* are true:
* <p>
* <ul>
* <li> The NEXT is associated with the UNDO, NEXT language statement or
* with an ON phrase that has UNDO, NEXT as the result of the given
* condition (e.g. ON ENDKEY UNDO, NEXT). It is important to note
* that Progress does not provide infinite loop protection for the
* NEXT language statement, so there is no conversion to LEAVE in
* that case.
* <li> The target block (whether implicitly determined by the lack of a
* label or explicitly determined by the presence of a label) must
* also have been the target block for the UNDO operation. It is
* possible for the NEXT to be targetted at a more enclosing block
* than the UNDO, in which case there is no conversion to LEAVE.
* <li> The target block type must be REPEAT, REPEAT WHILE or DO WHILE.
* These are the only loop types that normally convert RETRY to
* LEAVE instead of NEXT. Likewise, these are the only loop types
* which honor infinite loop protection for NEXT by converting it
* to a LEAVE.
* <li> Infinite loop protection must not have been disabled for the
* target block. In this case, any form of PAUSE is honored as
* one of the mechanisms to disable infinite loop protection.
* </ul>
* <p>
* Since NEXT is only possible on a looping block, if the target block
* is not a looping block then this method will return <code>false</code>.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param label
* The label of the target block for the NEXT action.
*
* @return <code>true</code> for NEXT, <code>false</code> for LEAVE.
*/
private static boolean honorNext(WorkArea wa, String label)
{
BlockDefinition blk = null;
int lvl = -1;
// check if the NEXT is targetted at the current block
if (label == null || label.length() == 0)
{
// get the current block
lvl = wa.blocks.size() - 1;
blk = wa.blocks.peek();
}
else
{
// lookup the block target by its label
lvl = findStackLevel(wa, label);
if (lvl >= 0)
blk = wa.blocks.elementAt(lvl);
}
// protect from NPE, but that should not happen unless a bogus label is passed
if (blk != null)
{
// any non-looping block type always converts NEXT to LEAVE
if (blk.type == DO ||
blk.type == FINALLY ||
blk.type == FOR_BLOCK ||
blk.type == FOR_BLOCK_WHILE ||
blk.type == FOR_BLOCK_TO ||
blk.type == FOR_BLOCK_TO_WHILE ||
blk.type == EXTERNAL_PROC ||
blk.type == INTERNAL_PROC ||
blk.type == FUNCTION ||
blk.type == TRIGGER ||
blk.type == DATABASETRIGGER)
{
return false;
}
// make sure the target block is one of the looping blocks that can
// cause NEXT to convert to LEAVE
if (blk.type == DO_WHILE ||
blk.type == REPEAT ||
blk.type == REPEAT_WHILE)
{
// conversion only happens if there is an active UNDO operation
// targetted at the same block
if (blk.isUndoLevel)
{
boolean disable = false;
// this next algorithm is needed because normally, the popping
// of each block off the stack handles the "rollup" of the
// ILP state, but this method is being called before that
// rollup can occur so we simulate it here to allow us to come
// to the same conclusion
// walk up the stack to find if ILP or PAUSE has occurred
// anywhere between the current block and the target block,
// since disabling infinite loop protection or any PAUSE
// causes the conversion to be avoided
for (int i = wa.blocks.size() - 1; i >= lvl; i--)
{
BlockDefinition next = wa.blocks.elementAt(i);
if (!next.loopProtection || next.hadPause)
{
disable = true;
break;
}
}
// if ILP is not disabled
if (!disable)
{
// convert to LEAVE
return false;
}
}
}
}
// honor the NEXT, don't convert to LEAVE
return true;
}
/**
* Determines if the currently executing block should be retried. This
* is where infinite loop protection is implemented if it is enabled.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return <code>true</code> if the block (whether or not it is a loop)
* should be retried.
*/
private static boolean needsRetry(WorkArea wa)
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
boolean noPause = true;
// a PAUSE sometimes acts to disable infinite loop protection
if (blk.hadPause)
{
// depending solely on the current block type
if (blk.type == DO_WHILE ||
blk.type == REPEAT ||
blk.type == REPEAT_WHILE)
{
noPause = false;
}
// also can happen in other block types IF this is a retry caused
// by the raising of an ENDKEY condition
if (blk.endkeyRetry)
{
if (blk.type == DO ||
blk.type == FINALLY ||
blk.type == DO_TO ||
blk.type == REPEAT_TO ||
blk.type == FOR_BLOCK ||
blk.type == FOR_BLOCK_WHILE ||
blk.type == FOR_BLOCK_TO ||
blk.type == FOR_BLOCK_TO_WHILE ||
blk.type == FOR_LOOP ||
blk.type == FOR_LOOP_WHILE ||
blk.type == FOR_LOOP_TO ||
blk.type == FOR_LOOP_TO_WHILE)
{
noPause = false;
}
}
}
boolean bypass = false;
// in certain block types when a RETRY is caused by an ENDKEY, the
// normal infinite loop protection mechanism is bypassed depending on
// an internal counter; this causes a kind of "stutter" effect where
// a RETRY on some iterations will be a real RETRY and other iterations
// it will be converted to a NEXT
if (blk.endkeyRetry && blk.loopProtection && noPause)
{
// looping blocks that modify state on each pass are the only
// blocks that get this stuttering behavior
if (blk.type == DO_TO ||
blk.type == REPEAT_TO ||
blk.type == FOR_LOOP ||
blk.type == FOR_LOOP_WHILE ||
blk.type == FOR_LOOP_TO ||
blk.type == FOR_LOOP_TO_WHILE)
{
// the ilpCount will start at -1 and then approach 1 (so the
// first 2 passes through here will convert to NEXT), then on
// the 3rd pass the ilpCount will back down to 0 (allowing the
// RETRY to occur normally), then the 4th pass will increment
// the ilpCount to 1 (converts to NEXT), then 5th pass will
// back down to 0 (RETRY), 6th pass (NEXT), 7th pass (RETRY)...
// causing a "stuttering" effect
// maintain the "stutter" counter
if (blk.ilpCount < 1)
{
// allow the retry to be converted to NEXT (all these block
// types should have blk.next == true)
blk.ilpCount++;
}
else
{
// cause a "stutter" (allow the RETRY to proceed as if ILP
// was disabled)
blk.ilpCount--;
bypass = true;
}
}
}
// honor infinite loop protection (noPause is the way that PAUSE
// disables infinite loop protection)
if (blk.retry && blk.loopProtection && noPause && !bypass)
{
// set flag to convert RETRY into LEAVE
if (!blk.next)
{
blk.pendingBreak = true;
}
// unless the special flag is set above, this is equivalent to NEXT
return false;
}
boolean result = blk.retry;
// is this a retry?
if (result)
{
// reset our retry state as if this was a fresh pass at the block
// except the wasRetried will be true this time
blk.retry = false;
blk.loopProtection = true;
blk.hadPause = false;
blk.endkeyRetry = false;
blk.pendingBreak = false;
blk.wasRetried = true;
blk.rolledBack = false;
blk.isUndoLevel = false;
// this is the last point before the retry pass of the loop actually
// occurs, notify the finalizables that a retry is occurring
processRetry(wa, blk);
}
return result;
}
/**
* Accesses the state of whether there is a pending break that must be
* honored. This would have been generated by the infinite loop
* protection.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return <code>true</code> if a break is pending for this block.
*/
private static boolean isBreakPending(WorkArea wa)
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
return blk.pendingBreak;
}
/**
* Force an error to be generated such that it will be caught in the
* caller of this method. The stack will be unwound and normal error
* processing bypassed until this method returns. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method,
* the "normal" stack unwinding approach cannot be used because the
* exception cannot be thrown. Instead, the error manager will be updated
* with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a
* <code>return</code> statement which will maintain the proper Progress
* behavior. The caller can check the error manager to determine that an
* error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top
* level block be found. The silent error flag of the caller is cached
* there.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param errNum
* The identifier of the error. When not-null, the error message will be registered.
* @param text
* The text to use as the error message in the exception.
*
* @throws ErrorConditionException
*/
private static void triggerErrorInCaller(WorkArea wa, Integer errNum, String text)
throws ErrorConditionException
{
triggerErrorInCaller(wa, errNum, text, true, true);
}
/**
* Force an error to be generated such that it will be caught in the
* caller of this method. The stack will be unwound and normal error
* processing bypassed until this method returns. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method,
* the "normal" stack unwinding approach cannot be used because the
* exception cannot be thrown. Instead, the error manager will be updated
* with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a
* <code>return</code> statement which will maintain the proper Progress
* behavior. The caller can check the error manager to determine that an
* error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top
* level block be found. The silent error flag of the caller is cached
* there.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param errNum
* The identifier of the error. When not-null, the error message will be registered.
* @param text
* The text to use as the error message in the exception.
* @param errFlag
* Flag indicating the ERROR-ERROR:ERROR is set.
* @param prefix
* Prefix or not with two asterics when building the error message text.
*
* @throws ErrorConditionException
*/
private static void triggerErrorInCaller(WorkArea wa,
Integer errNum,
String text,
boolean errFlag,
boolean prefix)
throws ErrorConditionException
{
BlockDefinition blk = nearestTopLevel(wa);
if (blk != null && blk.suppressError)
{
if (errNum != null)
{
wa.em.addError(errNum, text, errFlag, prefix);
}
wa.em.setPending(true);
}
else
{
// turn on the ignore flag
wa.ignoreError = true;
// throw the error
if (errNum != null)
{
throw new ErrorConditionException(errNum, text, true);
}
else
{
throw new ErrorConditionException(-1, text, true);
}
}
}
/**
* Force an error to be generated such that it will be caught in the
* caller of this method. The stack will be unwound and normal error
* processing bypassed until this method returns. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method,
* the "normal" stack unwinding approach cannot be used because the
* exception cannot be thrown. Instead, the error manager will be updated
* with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a
* <code>return</code> statement which will maintain the proper Progress
* behavior. The caller can check the error manager to determine that an
* error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top
* level block be found. The silent error flag of the caller is cached
* there.
*
* @param lex
* The legacy OO-like error to throw.
*
* @throws ErrorConditionException
*/
private static void triggerErrorInCaller(WorkArea wa, LegacyErrorException lex)
throws ErrorConditionException
{
BlockDefinition blk = nearestTopLevel(wa);
if (blk != null && blk.suppressError)
{
wa.em.setPending(true);
}
// throw the error
throw lex;
}
/**
* Check if this block's caller requires errors to be suppressed.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return {@link BlockDefinition#suppressError} for {@link #nearestTopLevel} block.
*
*/
private static boolean isSuppressError(WorkArea wa)
{
BlockDefinition blk = nearestTopLevel(wa);
return (blk != null && blk.suppressError);
}
/**
* Force a retry of the current block or the scope named by the given
* label, ensuring that the flow of control will not naturally continue.
* A {@link RetryUnwindException} will be thrown to unwind the stack
* to the given scope, even if the scope is the current block. This
* unwinding is dependent upon catch blocks being implemented with a
* corresponding call to {@link #honorRetry}.
* <p>
* <b>This method will NEVER naturally return since an exception is ALWAYS
* thrown.</b> This method should generally not be called from within a
* catch block if the intention is to retry the current block since it
* would be outside of the block's associated <code>try</code>. In that
* case, only an enclosing block could be retried.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param label
* The name of the scope to retry. Specify <code>null</code> to
* retry the scope that has a pending rollback OR to retry
* the current scope if there is no pending rollback.
*
* @throws RetryUnwindException
* This method always generates this exception to ensure that
* natural processing will not continue.
*/
private static void forceRetry(WorkArea wa, String label)
throws RetryUnwindException
{
int level = findStackLevel(wa, label);
if (level == -1)
{
if (wa.rollbackPending)
{
// force retry at the same level as the pending rollback
level = wa.rollbackScope;
}
else
{
if (!wa.isTransaction())
{
// in a non-transaction block we still may have an implicit
// target specified during rollback but it would have been
// stored in the retryScope var (it may have been set to -1
// too, that is OK)
level = wa.retryScope;
}
}
}
// is the current block the target?
if (level == -1)
{
// pretend that we are doing a "normal" unwind
level = wa.blocks.size() - 1;
}
// record the scope for the pending retry
wa.retryScope = level;
// abort current processing and jump to end of current block OR unwind
// the stack to the specified block
throw new RetryUnwindException("Unwinding to stack level " + level);
}
/**
* Force a retry of the current block or the scope named by the given
* label. If a containing scope is the target of the retry, then
* a {@link RetryUnwindException} will be thrown to unwind the stack
* to the given scope. This unwinding is dependent upon catch blocks
* being implemented with a corresponding call to {@link #honorRetry}.
* <p>
* If the current block is not the target, the retry is deferred by
* marking the target block and then unwinding the stack. Otherwise,
* this method will return naturally. This method is only appropriate
* to be called from within <code>catch</code> blocks since a natural
* return within a <code>catch</code> block will fall through to allow
* the retry loop to engage.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param label
* The name of the scope to retry. Specify <code>null</code> to
* retry the scope of that has a pending rollback OR to retry
* the current scope if there is no pending rollback.
*
* @throws RetryUnwindException
*/
private static void triggerRetry(WorkArea wa, String label)
throws RetryUnwindException
{
// TODO: if this is a routine block, then check if this retry needs to convert to ERROR
// TODO: exclude destructors and UI triggers
int level = findStackLevel(wa, label);
if (level == -1)
{
if (wa.rollbackPending)
{
// force retry at the same level as the pending rollback
level = wa.rollbackScope;
}
else
{
if (!wa.isTransaction())
{
// in a non-transaction block we still may have an implicit
// target specified during rollback but it would have been
// stored in the retryScope var (it may have been set to -1
// too, that is OK)
level = wa.retryScope;
}
}
}
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
if (level == -1 || level == (wa.blocks.size() - 1))
{
blk.retry = true;
}
else
{
// record the scope for the pending retry
wa.retryScope = level;
// unwind the stack to that point
throw new RetryUnwindException("Unwinding to stack level " + level);
}
}
/**
* Detect if the nearest top level block is a database trigger block
* @param wa
* Context local work area. Must not be {@code null}.
* @return
* True if the nearest top level block is a database trigger, false otherwise
*/
public static boolean isTopLevelDatabaseTrigger(WorkArea wa)
{
BlockDefinition blk = nearestTopLevel(wa);
return blk.databaseTrigger;
}
/**
* Dispatch the given exception to all registered stop condition handlers
* to see if any requires that the STOP condition be vetoed at the
* current scope. If any of the registered handlers indicate that the STOP
* condition should not be honored at this level, the exception is
* rethrown, presumably to be honored at a higher scope, or not at all. In
* the latter case, the program running in the current context will abort.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param exc
* The exception to be checked.
*
* @throws StopConditionException
* if the STOP condition is not honored at this scope.
*/
private static void honorStop(WorkArea wa, StopConditionException exc)
throws StopConditionException
{
for (StopConditionVetoHandler handler : wa.stopHandlers)
{
if (handler.vetoStop(exc))
{
// re-throw the exception but first it will maintain the rollback
// state/processing
abnormalEnd(wa, exc, false);
}
}
// No handlers want to veto the STOP at this scope - allow the STOP
// to be executed
}
/**
* Check if there is a pending retry of the current block and if so then
* enable retry. This is the "last half" of the processing to unwind the
* stack up to a containing scope that is the target of a retry. The
* "first half" of the unwinding is implemented by throwing a
* {@link RetryUnwindException}. This unwinding is dependent upon catch
* blocks being implemented to call this method.
* <p>
* If the current block is not the target for the pending retry, the
* exception is re-thrown.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param rue
* The exception to honor or ignore.
*
* @throws RetryUnwindException
*/
private static void honorRetry(WorkArea wa, RetryUnwindException rue)
throws RetryUnwindException
{
if (wa.retryScope == (wa.blocks.size() - 1))
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
blk.retry = true;
wa.retryScope = -1;
}
else
{
// we cannot propagate the exception past the full transaction
// level without causing a rollback(), unless there is a rollback
// already pending in which case it will be naturally be handled
// by popScope() when the full transaction exits, so don't force a
// rollback again (calling this more than once must not be done
// while there is a pending rollback); note that this is only safety
// code since "well formed" converted 4GL code should never cause
// this to occur since the UNDO and RETRY must always be targeted
// at the same block AND the UNDO (a call to rollback()) should
// always be made before the RETRY is triggered
if (wa.isFullTransaction() && !hadRollback())
{
rollback(null, rue);
}
// we aren't there yet, re-throw the exception
throw rue;
}
}
/**
* Check if the current error must unwind all the way to the caller of
* the top-level block in this method (if the ignore flag is set). If
* this is the case then the error is re-thrown. Before this occurs, if
* the current block is the top-level block of this method, then the
* ignore flag will be cleared. If the ignore flag is <code>false</code>
* then this method returns silently.
* <p>
* This code also honors any nested error that was generated while another
* error was being processed AND while nested error mode was enabled. Any
* such stored error will be re-thrown here.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param err
* The exception to honor or ignore.
*
* @throws ErrorConditionException
*/
private static void honorError(WorkArea wa, ErrorConditionException err)
throws ErrorConditionException
{
if (wa.ignoreError ||
isTopLevelDatabaseTrigger(wa))
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
if (blk.topLevel)
{
// the next catch block will handle this error (it will be in the caller)
wa.ignoreError = false;
// this bypasses the commit
blk.rolledBack = true;
}
// TODO: should this call abnormalEnd() instead so that the rollback
// state/processing is maintained/handled?
// continue to unwind
throw err;
}
// nested error support
if (wa.nestedError != null)
{
ConditionException ece = wa.nestedError;
// reset our nested error
wa.nestedError = null;
// TODO: should this call abnormalEnd() instead so that the rollback
// state/processing is maintained/handled?
throw ece;
}
}
/**
* Notifies the transaction manager that an unexpected exception or error
* has occurred and that any transaction must be aborted. This will
* maintain the state of the system properly, including rolling back any
* open transaction. Once the state is properly maintained, the triggering
* <code>Throwable</code> will be re-thrown as a
* <code>RuntimeException</code>.
* <p>
* If the exception is of type <code>ConditionException</code> or
* <code>RetryUnwindException</code>, this method will do nothing since
* these are used for normal flow of control purposes and are used in
* close coordination with the transaction manager's state. They need
* no other special behavior to be safe. This method is designed to
* handle exceptions (like <code>NullPointerException</code>) which are
* not valid in a converted 4GL program.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param thr
* The unexpected exception or error.
* @param needsNotify
* <code>true</code> if condition notification must be executed
* (only happens if the <code>thr</code> is a condition).
*
* @throws RuntimeException
*/
private static void abnormalEnd(WorkArea wa, Throwable thr, boolean needsNotify)
throws RuntimeException
{
if (thr instanceof ConditionException)
{
if (needsNotify)
{
notifyCondition(wa, (ConditionException) thr);
}
// we cannot propagate the exception past the full transaction
// level without causing a rollback() except for QUIT which has
// the unusual behavior of committing the transaction UNLESS one
// specifies an ON QUIT phrase inside the transaction block or a
// nested block which encloses the raised condition (note that in
// that case this method will never be called so it will work fine)
if (wa.isFullTransaction() && !(thr instanceof QuitConditionException))
{
// if a rollback is already pending, then it will be naturally
// handled by popScope() when the full transaction exits, so
// don't force a rollback again (calling this more than once
// must not be done while there is a pending rollback)
if (!hadRollback())
{
rollback(wa, null, thr);
}
}
ConditionException ce = (ConditionException) thr;
throw ce;
}
else if (thr instanceof RetryUnwindException)
{
// this should never really get called since there should always
// be an explicit catch block for this BEFORE any Throwable catch
// block, but for safety purposes...
RetryUnwindException rue = (RetryUnwindException) thr;
honorRetry(wa, rue);
}
else
{
SessionUtils.clientDisconnected();
if (!isCausedBy(thr, ApplicationRequestedStop.class))
{
LOG.severe("Abnormal end; original error:", thr);
}
boolean storeDeferred = (wa.deferredError == null);
try
{
if (storeDeferred)
{
// if there is no existing deferred error, store the current
// error, ensuring errors thrown in this try block do not mask
// the cause of the abnormal end, but are still logged by
// deferError()
wa.deferredError = thr;
}
notifyCondition(wa, BlockManager.Condition.UNKNOWN);
rollback(wa, null, thr);
}
finally
{
if (storeDeferred)
{
// reset deferred error to its previous state
wa.deferredError = null;
}
}
if (thr instanceof RuntimeException)
{
// the exception is already the right type so we don't have to
// wrap it again (this avoids unnecessarily long chained log
// entries
throw (RuntimeException) thr;
}
else
{
// wrap the exception in a type that will propagate all the
// way "out" as an abend
throw new RuntimeException(thr);
}
}
}
/**
* Returns if a Throwable is caused by a Throwable of the class to check against.
*
* @param thr
* The Throwable to check.
* @param clazz
* The Throwable class to check against.
*
* @return <code>true</code> if the Throwable is caused a Throwable of the provided class,
* <code>false</code> otherwise.
*/
private static <T extends Throwable> boolean isCausedBy(Throwable thr, Class<T> clazz)
{
if (thr == null)
{
return false;
}
if (thr.getClass().equals(clazz))
{
return true;
}
return isCausedBy(thr.getCause(), clazz);
}
/**
* Reports if the current block is a top-level block (this corresponds
* with a method-level block).
*
* @param wa
* Context local work area.
*
* @return <code>true</code> if top-level block, else <code>false</code>.
*/
private static boolean isTopLevelBlock(WorkArea wa)
{
BlockDefinition blk = wa.blocks.peek();
return blk.topLevel;
}
/**
* Reports if the current block is a global scoped block. For normal contexts:
* <code>true</code> if the current block is the only block on the stack of block scopes; for
* appserver agent contexts: <code>true</code> if the current block is one of two outermost
* blocks ("appserver-agent" and "startup") on the stack of block scopes.
*
* @param wa
* Context local work area.
*
* @return See above.
*/
private static boolean isGlobalBlock(WorkArea wa)
{
int size = wa.blocks.size();
return size == 1 || (size == 2 && wa.isRemote());
}
/**
* Search the stack of scopes and return the level of the first top level block found.
*
* @param wa
* Context local work area.
*
* @return The stack level of the nearest enclosing top level block or
* -1 if there is no block marked as a top level block.
*/
private static int findNearestTopLevel(WorkArea wa)
{
// walk backwards up the stack
for (int i = wa.blocks.size() - 1; i >= 0; i--)
{
BlockDefinition blk = wa.blocks.elementAt(i);
if (blk.topLevel)
{
return i;
}
}
return -1;
}
/**
* Get the rollup value for the enclosing block. If <code>true</code>,
* then the current block's exit will cause the enclosing (containing)
* block's interactions counter to be increased by the current block's
* interactions counter. Otherwise, no aggregation will occur.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return The current block's rollup value.
*/
private static boolean isEnclosingRollup(WorkArea wa)
{
int parent = wa.blocks.size() - 2;
// by default, the rollup value is true
if (parent < 0)
return true;
BlockDefinition blk = wa.blocks.elementAt(parent);
return blk.rollup;
}
/**
* Resets all registered <code>Resettable</code> objects or makes them
* clear their lists of changes that should be reset.
*
* @param wa
* The {@link WorkArea} instance.
* @param clearOnly
* If <code>true</code> then clear the lists of changes that should
* be reset, otherwise perform reset of these changes.
*/
private static void processResettables(WorkArea wa, boolean clearOnly)
{
for (int i = 0; i < wa.resettables.size(); i++)
{
Resettable resettable = wa.resettables.get(i);
resettable.resetState(clearOnly);
}
}
/**
* Adds the object into a scoped dictionary of objects requiring UNDO support. This registry
* is used to backup the objects at the beginning of transactions or sub-transactions.
* <p>
* These registrations naturally are removed when the associated scope is removed. However,
* the global scope is long-lived. Special care is necessary to prevent the same instance of
* global shared variables being added multiple times.
*
* @param undoTarget
* The object reference to be rolled back on failure. This
* cannot be <code>null</code>.
* @param transLevel
* The transaction level to set for this undoable.
* @param persistent
* Flag indicating if the undoable is defined in a persistent procedure
*/
private static void registerUndo(LazyUndoable undoTarget, int transLevel, boolean persistent)
{
if (undoTarget == null)
{
return;
}
// if we are starting a persistent procedure, then the undoables must register all
// the way up the stack, and not just until the defining scope is reached: this is
// because an undo can be trigger by a previous block, outside of the persistent
// procedure, after the persistent procedure has finished its external scope
// the transLevel is subtracted 1 because we register in the current tx block
undoTarget.markUndoable(persistent ? -1 : transLevel - 1);
}
/**
* Search the stack of scopes and return the type of first top level block found.
*
* @param wa
* Context local work area.
*
* @return The nearest enclosing top level block or {@link BlockType#UNKNOWN} if
* there is no block marked as a top level block.
*/
private static BlockType getNearestTopLevelType(WorkArea wa)
{
BlockDefinition top = nearestTopLevel(wa);
return (top == null) ? UNKNOWN : top.type;
}
/**
* Search the stack of scopes and return the first top level block found.
*
* @param wa
* Context local work area.
*
* @return The nearest enclosing top level block or <code>null</code> if
* there is no block marked as a top level block.
*/
private static BlockDefinition nearestTopLevel(WorkArea wa)
{
Stack<BlockDefinition> blocks = wa.blocks;
for (int idx = blocks.size() - 1; idx >= 0; idx--)
{
BlockDefinition blk = blocks.get(idx);
if (blk.topLevel)
{
// done!
return blk;
}
}
return null;
}
/**
* Returns the index of the full transaction block, or -1 if none exists.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return The index of the full transaction block, or -1 if none exists.
*/
private static int getFullTransactionBlock(WorkArea wa)
{
Stack<BlockDefinition> blocks = wa.blocks;
for (int i = 0; i < blocks.size(); i++)
{
if (blocks.get(i).full)
{
return i;
}
}
return -1;
}
/**
* Search the stack of scopes and return the level of the first external
* block found.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return The stack level of the nearest enclosing external block or
* -1 if there is no block marked as an external block. Level
* number is 1-based starting from the outermost scope.
*/
private static int findNearestExternal(WorkArea wa)
{
ListIterator<BlockDefinition> iter =
wa.blocks.listIterator(wa.blocks.size());
int level = wa.blocks.size() + 1;
// walk backwards up the stack of block definitions
while (iter.hasPrevious())
{
BlockDefinition blk = iter.previous();
level--;
if (blk.external)
{
return level;
}
}
return -1;
}
/**
* This method is called after the initialization of a FOR or FOR EACH
* block is finished. It will save all the {@link QueryOffEndListener}s
* associated with the registered {@link WorkArea#offEndQueries queries}.
*
* @param wa
* Context local work area. Must not be {@code null}.
*/
private static void stopOffEndRegistration(WorkArea wa)
{
Set<QueryOffEndListener> listeners = wa.offEndListeners.peek();
Set<P2JQuery> queries = wa.offEndQueries.pop();
if (!queries.isEmpty())
{
if (listeners == Collections.EMPTY_SET)
{
listeners = Collections.newSetFromMap(new IdentityHashMap<>());
wa.offEndListeners.pop();
wa.offEndListeners.push(listeners);
}
for (P2JQuery query : queries)
{
ArrayList<QueryOffEndListener> ql = query.getOffEndListeners();
for (int i = 0; i < ql.size(); i++)
{
listeners.add(ql.get(i));
}
}
}
wa.offEndRegistration = false;
}
/**
* Register the passed query for query off-end events. The passed queries
* are all the queries created in the init method for a FOR or FOR EACH
* block.
* <p>
* These queries will be able to end the loop when the last record is
* reached, by overriding any user-defined END-KEY action. The action
* wich is executed is the default UNDO, LEAVE action for END-KEY.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param query
* The query which needs to be registered.
*/
private static void registerOffEndQuery(WorkArea wa, P2JQuery query)
{
if (wa.offEndRegistration)
{
wa.offEndQueries.peek().add(query);
}
}
/**
* Check if the given listener is registered for query off-end events and
* can override the default END-KEY action (which is UNDO, LEAVE), when
* the current block is FOR or FOR EACH.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param listener
* A listener to be checked if is registered for off-end events.
*
* @return <code>true</code> if the listener is registered for query
* off-end event.
*/
private static boolean isOffEndListener(WorkArea wa, QueryOffEndListener listener)
{
Set<QueryOffEndListener> listeners = wa.offEndListeners.peek();
return listeners.contains(listener);
}
/**
* Iterate all registered off-end listeners and call the
* {@link QueryOffEndListener#initialize() initialize} or
* {@link QueryOffEndListener#finish() finish} method, depending on the
* flag.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param init
* <code>true</code> if the listeners are initialized; else, the
* {@link QueryOffEndListener#finish()} method is called.
*/
private static void processOffEndListeners(WorkArea wa, boolean init)
{
Set<QueryOffEndListener> listeners = wa.offEndListeners.peek();
for (QueryOffEndListener qoel : listeners)
{
if (init)
{
qoel.initialize();
}
else
{
qoel.finish();
}
}
}
/**
* Get the topmost set of off-end {@link QueryOffEndListener listeners}.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param clear
* <code>true</code> if the top-most off-end listeners set
* should be cleared.
*/
private static Set<QueryOffEndListener> getOffEndListeners(WorkArea wa, boolean clear)
{
Set<QueryOffEndListener> top = wa.offEndListeners.peek();
try
{
if (!top.isEmpty())
{
Set<QueryOffEndListener> res = Collections.newSetFromMap(new IdentityHashMap<>());
res.addAll(top);
return res;
}
else
{
return Collections.emptySet();
}
}
finally
{
if (clear)
{
top.clear();
}
}
}
/**
* Register the single output parameter assigner for the current block.
* This object will be deregistered by the {@link BlockManager} when it is
* time to process assign-backs from function/procedure output parameters
* to the database record fields with which they are associated.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param opa
* An output parameter assigner.
*
* @throws IllegalStateException
* if a second output parameter assigner is registered for a block
* before the current one is deregistered. There can be only one.
*/
private static void registerOutputParameterAssigner(WorkArea wa, OutputParameterAssigner opa)
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
if (blk.outputParmAssigner != null)
{
throw new IllegalStateException(
"Illegal attempt to register output parameter assigner");
}
blk.outputParmAssigner = opa;
}
/**
* Defer handling of an exception thrown by a commit/rollback notification
* callback on a registered {@link Commitable}, so that iteration or pop
* scope processing can be continued without interruption. This ensures
* that all <code>Commitable</code>s registered after the errant one still
* receive their commit/rollback notifications, and that (in the case of
* popping a scope), registered {@link Scopeable}s still receive their
* scope finished notifications.
* <p>
* If no error is already pending, <code>exc</code> is stored, to be thrown
* upon conclusion of processing the current block scope or iteration. If
* an error is already pending, <code>exc</code> is discarded. In either
* case, <code>exc</code> is logged if error-level logging is enabled.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param action
* A brief description of the action occurring at the time the
* error was encountered, such as "commit" or "master rollback".
* @param thr
* The error caught during processing of a commit or rollback
* notification.
* @param silent
* If <code>true</code> then exception is not logged.
*
* @see WorkArea#handleDeferredError
*/
private static void deferError(WorkArea wa, String action, Throwable thr, boolean silent)
{
// when intermixing Progress-specific and non-Progress aware runtime
// layers, it is possible for ConditionException causes to be hidden
// within other exception types; this processing follows the causal
// chain and un-hides any ConditionException that was obscured
Throwable cause = thr;
Throwable condition = thr;
boolean interrupted = false;
boolean silentUnwind = false;
while (cause != null)
{
if (cause instanceof ConditionException)
{
condition = cause;
}
if (cause instanceof InterruptedException)
{
interrupted = true;
}
if (cause instanceof SilentUnwindException)
{
silentUnwind = true;
}
cause = cause.getCause();
}
if (!silent)
{
// if the condition is a STOP condition generated by a thread
// interruption via CTRL+C, then we deferr the error, but change the
// log level from SEVERE to FINE
Level logLvl = silentUnwind ||
(condition instanceof StopConditionException &&
interrupted) ? Level.FINER
: Level.SEVERE;
if (LOG.isLoggable(logLvl))
{
String msg = "Error processing " + action + "; ";
if (wa.deferredError == null)
{
msg += "deferring throw until block/iteration completes";
}
else
{
msg += "deferred error already pending; new error discarded";
}
log(logLvl, "deferError", msg, thr);
}
}
if (wa.deferredError == null)
{
// save the error
wa.deferredError = condition;
}
}
/**
* Notify all registered {@link BatchListener} objects that a batch is
* either starting or stopping.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param start
* <code>true</code> if this is the start of a batch, otherwise
* <code>false</code>.
*/
private static void processBatchNotifications(WorkArea wa, boolean start)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE,
"processBatchNotifications",
"start_batch = " + Boolean.toString(start));
}
for (BatchListener listener : wa.batchListeners)
{
listener.batchNotify(start);
}
}
/**
* Allows a loop iteration event to occur in the transaction manager,
* which triggers commit processing before the next iteration of the
* loop begins.
* <p>
* This hook is called at the top of the loop, just after the loop test
* has been passed. If the loop test fails, then this will not be called.
* This code will be called inside the block itself, as the first thing
* that is done.
* <p>
* Note that this is only called on the second and subsequent iterations
* of the block, no matter how the iteration occurred (via natural control
* flow through the bottom of an iterating block OR via a Java
* <code>continue</code> statement that caused a jump to the top of the
* loop). Clearing the retry logic and rollback logic is important
* whether or not this is a loop since non-loops can be retried (via
* condition processing) or iterated via <code>continue</code>.
* <p>
* All registered {@link Finalizable Finalizables} are notified of the
* iteration via their {@link Finalizable#iterate} callback method.
* <p>
* If the block is not a loop, commit/rollback processing and
* <code>Finalizable</code> processing will NOT be executed.
* <p>
* This must never be called outside of a bracketed pair of
* {@link #pushScope} and {@link #popScope} calls.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @param wa
* The {@link WorkArea} instance.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
private static void iterateWorker(WorkArea wa)
throws StopConditionException
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "iterateWorker", null);
}
// peek at the block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
// transaction related processing
if (blk.loop && wa.isTransaction())
{
// is there a pending rollback or have we rolled back in this scope?
if (wa.isRollbackPending())
{
// determine if a pending rollback refers to this scope
if (rollbackCurrentScope(wa, wa.getRollbackScope()))
{
processRollback(wa, blk);
}
}
else if (!blk.rolledBack)
{
// OK, this is a case where we need to COMMIT!
processCommit(wa, blk);
}
}
// reset our retry/commit logic
blk.committed = false;
blk.retry = false;
blk.loopProtection = true;
blk.hadPause = false;
blk.endkeyRetry = false;
blk.pendingBreak = false;
blk.wasRetried = false;
blk.rolledBack = false;
blk.isUndoLevel = false;
blk.ilpCount = 0;
// notify listeners of the iteration
if (blk.loop)
{
processFinalizables(wa, blk, true);
}
try
{
// FINALLY block support (this MUST be AFTER commit/rollback and finalizables are processed); at
// this location a full transaction still appears to be open in FWD but in the 4GL will report
// TRANSACTION == false for the full-transaction level (sub-transaction levels still show being
// inside the transaction); this means we must force a save/clear/restore of the transaction state
// for the duration of the FINALLY block execution
try
{
processTerminationBlock(wa, blk, blk.finallyBlock);
}
finally
{
blk.finallyBlock = null;
}
}
finally
{
wa.handleDeferredError();
}
}
/**
* Creates or updates the backup set for all objects needing rollback
* support in the current block, if a transaction is active. This is
* split up from {@link #pushScope} and {@link #blockSetup} due to the
* semantics of Progress in which the backup set is created or refreshed
* after the loop control variables have been modified.
*
* @param wa
* The {@link WorkArea} instance.
* @param blk
* The block for which a backup must be made or <code>null</code>
* to use the current block on top of the stack.
*/
private static void backupWorker(WorkArea wa, BlockDefinition blk)
{
// read the block def at the top of the stack
if (blk == null)
{
blk = wa.blocks.peek();
}
// transactions and sub-transactions trigger backup processing (blk.undoables will be
// null when a transaction is not active OR when in a NO_TRANSACTION block OR when in
// a transaction and no changes have been registered)
if (blk.undoables != null)
{
// this is an iteration, update the backup set for the next pass
updateBackupSet(blk.undoables);
}
}
/**
* Determine if the current scope represents a transaction or
* sub-transaction and if so, whether the given level explicitly refers
* to this scope OR if the given level is -1, then this implicitly refers
* to this scope.
* <p>
* Even if the level is not -1 (it must refer to an enclosing block), if
* the given block is a full transaction block (the block at which the
* transaction is scoped), then the rollback must occur at this point
* instead of deferring the rollback until the transaction is no longer
* active.
*
* @param wa
* The {@link WorkArea} instance.
* @param level
* The stack level of the scope to be rolled back.
*
* @return <code>true</code> if the above conditions hold.
*/
private static boolean rollbackCurrentScope(WorkArea wa, int level)
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
return !isNoTransactionLevel(blk.level) &&
(level == -1 || level == (wa.blocks.size() - 1) || blk.full);
}
/**
* Find the nearest enclosing block associated with the given label.
*
* @param label
* The block label to search for.
*
* @return The block found or <code>null</code> if there is no enclosing
* block with that label.
*/
@SuppressWarnings("unused")
private static BlockDefinition findBlock(String label)
{
if (label != null)
{
WorkArea wa = workArea.obtain();
int sz = wa.blocks.size();
// walk backwards up the stack
for (int i = sz - 1; i >= 0; i--)
{
BlockDefinition blk = wa.blocks.elementAt(i);
if (blk.label.equals(label))
{
return blk;
}
}
}
return null;
}
/**
* Find the stack level of the nearest enclosing block associated with the
* given label.
*
* @param wa
* Context local work area.
* @param label
* The block label to search for.
*
* @return The 0-based stack level for the block that was found or -1 if
* there is no enclosing block with that label.
*/
private static int findStackLevel(WorkArea wa, String label)
{
if (label != null)
{
int sz = wa.blocks.size();
// walk backwards up the stack
for (int i = sz - 1; i >= 0; i--)
{
BlockDefinition blk = wa.blocks.elementAt(i);
if (blk.label.equals(label))
{
return i;
}
}
}
return -1;
}
/**
* Worker that actually implements rollback by copying the backed up
* values into the original reference. Once all externally driven
* rollback processing (the {@link Undoable} interface) is done,
* notifications are provided to all registered {@link Commitable}
* instances in the current block's commitable list.
* <p>
* If the block is also the full transaction block, the last thing
* done is a single master rollback call to the instance registered
* for the {@link #registerTransactionCommit}.
*
* @param wa
* The {@link WorkArea} instance.
* @param blk
* The block to be rolled back.
*/
private static void processRollback(WorkArea wa, BlockDefinition blk)
{
if (blk.commitables == null)
{
return;
}
if (FwdServerJMX.JMX_DEBUG)
{
ROLLBACK.timer(() -> processRollbackImpl(wa, blk));
}
else
{
processRollbackImpl(wa, blk);
}
}
/**
* Worker that actually implements rollback by copying the backed up
* values into the original reference. Once all externally driven
* rollback processing (the {@link Undoable} interface) is done,
* notifications are provided to all registered {@link Commitable}
* instances in the current block's commitable list.
* <p>
* If the block is also the full transaction block, the last thing
* done is a single master rollback call to the instance registered
* for the {@link #registerTransactionCommit}.
*
* @param wa
* The {@link WorkArea} instance.
* @param blk
* The block to be rolled back.
*/
private static void processRollbackImpl(WorkArea wa, BlockDefinition blk)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "processRollback", null);
}
// mark the start of the critical section
CriticalSectionManager.beginSection();
try
{
try
{
wa.nowIterating = blk.commitables;
// notify commitables
ArrayList<Commitable> commitables = blk.commitables;
for (int i = 0; i < commitables.size(); i++)
{
Commitable target = commitables.get(i);
try
{
target.rollback(blk.full);
}
catch (Throwable thr)
{
deferError(wa, "rollback", thr, false);
}
}
}
finally
{
wa.nowIterating = null;
}
// process the master rollback
if (blk.full)
{
wa.notifyMasterCommit(true);
}
// mark the work area to note that rollback has been completed, which
// clears the deferred rollback state; subsequent calls to the
// isRollbackPending() method will return false.
wa.rollbackScope = -1;
wa.rollbackPending = false;
wa.rollbackPendingCause = null;
// process the externally driven undo
if (blk.undoables != null)
{
ArrayList<UndoablePair> undoables = blk.undoables;
for (int i = 0; i < undoables.size(); i++)
{
UndoablePair next = undoables.get(i);
next.src.rollback(next.copy);
}
}
}
finally
{
// stop the critical section
CriticalSectionManager.endSection();
}
}
/**
* Worker that notifies all objects in the commit list that they must
* commit (persist any state).
* <p>
* On the last iteration of a loop or block which ends in success (in
* which the flow of control reaches the natural end of the block), both
* will be called in succession. However, other forms of exit which are
* still considered successful (i.e. return and break which are not the
* result of condition processing) will only execute <code>popScope</code>
* and <code>iterate</code> will be bypassed.
* <p>
* It is important to note that the list of commitables in blk is
* iterated in reverse order from that in which they are registered with
* the <code>TransactionManager</code>. This is done to ensure that
* database transactions (which are registered before their corresponding
* buffers) are always committed after all buffers are committed.
*
* @param wa
* The {@link WorkArea} instance.
* @param blk
* The scope to be committed.
*/
private static void processCommit(WorkArea wa, BlockDefinition blk)
{
if (blk.commitables == null)
{
return;
}
if (FwdServerJMX.JMX_DEBUG)
{
COMMIT.timer(() -> processCommitImpl(wa, blk));
}
else
{
processCommitImpl(wa, blk);
}
}
/**
* Worker that notifies all objects in the commit list that they must
* commit (persist any state).
* <p>
* On the last iteration of a loop or block which ends in success (in
* which the flow of control reaches the natural end of the block), both
* will be called in succession. However, other forms of exit which are
* still considered successful (i.e. return and break which are not the
* result of condition processing) will only execute <code>popScope</code>
* and <code>iterate</code> will be bypassed.
* <p>
* It is important to note that the list of commitables in blk is
* iterated in reverse order from that in which they are registered with
* the <code>TransactionManager</code>. This is done to ensure that
* database transactions (which are registered before their corresponding
* buffers) are always committed after all buffers are committed.
*
* @param wa
* The {@link WorkArea} instance.
* @param blk
* The scope to be committed.
*/
private static void processCommitImpl(WorkArea wa, BlockDefinition blk)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "processCommit", null);
}
if (blk.committed || !wa.processUndoables(blk))
{
// already done or nothing to do, just return
return;
}
try
{
wa.nowIterating = blk.commitables;
ArrayList<Commitable> commitables = blk.commitables;
for (int i = 0; i < commitables.size(); i++)
{
Commitable target = commitables.get(i);
try
{
target.commit(blk.full);
}
catch (Throwable thr)
{
deferError(wa, "commit", thr, false);
}
}
}
finally
{
wa.nowIterating = null;
}
// process the master commit
if (blk.full)
{
wa.notifyMasterCommit(false);
}
// remember that we have already handled the commit
blk.committed = true;
}
/**
* Worker that notifies all objects in the finalize list that the block
* is being exited or iterated.
*
* @param wa
* Context-local work area.
* @param blk
* The scope to be finalized.
* @param iterate
* <code>true</code> to call each target's <code>iterate</code>
* method or <code>false</code> to call <code>finished</code>.
*/
private static void processFinalizables(WorkArea wa,
BlockDefinition blk,
boolean iterate)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE,
"processFinalizables",
"iterate = " + Boolean.toString(iterate));
}
Object prevReferent = wa.pm.getProcessedProcedure();
Set<Finalizable>[] fini = blk.finalizables;
if (fini != null)
{
Object referent = null;
boolean delayed = false;
// can not call for start, as the procedure is not yet initialized
if (!iterate && isTopLevelBlock(wa) && !isGlobalBlock(wa))
{
referent = wa.pm._thisProcedure();
wa.pm.setProcessedProcedure(referent);
if (getNearestTopLevelType(wa) == EXTERNAL_PROC)
{
delayed = wa.pm.isThisProcedurePersistent() && !wa.pm._isDelayedDelete(referent);
}
}
try
{
wa.nowIterating = fini;
wa.op = iterate ? OP_ITERATE : OP_FINISHED;
for (int i = 0; i < fini.length; i++)
{
Set<Finalizable> l = fini[i];
if (l == null)
{
continue;
}
for (Finalizable target : l)
{
try
{
if (iterate)
{
target.iterate();
}
else
{
target.finished();
if (delayed)
{
wa.pm.addFinalizable(referent, target);
}
else
{
target.deleted();
}
}
}
catch (Throwable thr)
{
deferError(wa, (iterate ? "iterate" : "finished"), thr, false);
}
}
}
}
finally
{
wa.nowIterating = null;
wa.pm.setProcessedProcedure(prevReferent);
wa.op = OP_NONE;
}
}
// process the master finish/iterate
if (blk.full)
{
wa.notifyMasterFinish(iterate);
}
}
/**
* Worker that notifies all objects in the finalize list that the block is being entered
* (the {@code Finalizable.entry()} method will be called. No persistent changes to the
* transaction or block state will occur.
*
* @param wa
* The {@link WorkArea} instance.
* @param blk
* The scope to be retried.
*/
private static void processEntry(WorkArea wa, BlockDefinition blk)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "processEntry", null);
}
Set<Finalizable>[] fini = blk.finalizables;
if (fini != null)
{
try
{
wa.nowIterating = fini;
wa.op = OP_ENTRY;
for (int i = 0; i < fini.length; i++)
{
Set<Finalizable> l = fini[i];
if (l == null)
{
continue;
}
for (Finalizable target : l)
{
try
{
target.entry();
}
catch (Throwable thr)
{
deferError(wa, "entry", thr, false);
}
}
}
}
finally
{
wa.nowIterating = null;
wa.op = OP_NONE;
}
}
}
/**
* Worker that notifies all objects in the finalize list that the block
* is being retried. In addition, any pending rollback for this block
* is honored here.
*
* @param wa
* The {@link WorkArea} instance.
* @param blk
* The scope to be retried.
*/
private static void processRetry(WorkArea wa, BlockDefinition blk)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "processRetry", null);
}
// transaction related processing
if (wa.isTransaction())
{
// is there a pending rollback or have we rolled back in this scope?
if (wa.isRollbackPending())
{
// determine if a pending rollback refers to this scope
if (rollbackCurrentScope(wa, wa.getRollbackScope()))
{
// note that it is good that this method does NOT set the
// blk.rolledBack flag as otherwise a commit could not occur
// at the end of this retry iteration
processRollback(wa, blk);
}
}
}
Set<Finalizable>[] fini = blk.finalizables;
if (fini != null)
{
try
{
wa.nowIterating = fini;
wa.op = OP_RETRY;
for (int i = 0; i < fini.length; i++)
{
Set<Finalizable> l = fini[i];
if (l == null)
{
continue;
}
for (Finalizable target : l)
{
try
{
target.retry();
}
catch (Throwable thr)
{
deferError(wa, "retry", thr, false);
}
}
}
}
finally
{
wa.nowIterating = null;
wa.op = OP_NONE;
}
}
// process the master retry
if (blk.full)
{
wa.notifyMasterRetry();
}
try
{
// FINALLY block support (this MUST be AFTER commit/rollback and finalizables are processed); at
// this location a full transaction still appears to be open in FWD but in the 4GL will report
// TRANSACTION == false for the full-transaction level (sub-transaction levels still show being
// inside the transaction); this means we must force a save/clear/restore of the transaction state
// for the duration of the FINALLY block execution
try
{
processTerminationBlock(wa, blk, blk.finallyBlock);
}
finally
{
blk.finallyBlock = null;
}
}
finally
{
wa.handleDeferredError();
}
}
/**
* Get the BlockDefinition instance of the nearest block of one of the
* following types:
* <ul>
* <li>{@link BlockType#EXTERNAL_PROC external procedure}</li>
* <li>{@link BlockType#INTERNAL_PROC internal procedure}</li>
* <li>{@link BlockType#FUNCTION user-defined function}</li>
* <li>{@link BlockType#TRIGGER trigger}</li>
* <li>{@link BlockType#DATABASETRIGGER trigger}</li>
* <li>{@link BlockType#EDITING editing}</li>
* </ul>
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return See above.
*/
private static BlockDefinition nearestExecutingBlock(WorkArea wa)
{
Stack<BlockDefinition> blocks = wa.blocks;
ListIterator<BlockDefinition> iter = blocks.listIterator(blocks.size());
// walk backwards up the stack of block definitions
while (iter.hasPrevious())
{
BlockDefinition blk = iter.previous();
if (blk.type == EXTERNAL_PROC ||
blk.type == INTERNAL_PROC ||
blk.type == FUNCTION ||
blk.type == TRIGGER ||
blk.type == EDITING ||
blk.type == DATABASETRIGGER)
{
// done!
return blk;
}
}
return null;
}
/**
* Deregister the output parameter assigner for the current block, if any.
* It is expected that only the {@link BlockManager} will invoke this
* method.
*
* @param wa
* Context local work area. Must not be {@code null}.
*
* @return Output parameter assigner for this block, or <code>null</code>
* if none was registered.
*/
private static OutputParameterAssigner deregisterOutputParameterAssigner(WorkArea wa)
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
OutputParameterAssigner opa = blk.outputParmAssigner;
blk.outputParmAssigner = null;
return opa;
}
/**
* Adds the given object into the list of objects which require undo
* notifications for the next block that is opened.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block. In particular, it is necessary for
* objects that are constructed during a class' constructor/initializer
* (before the external procedure block has opened). Such objects cannot
* simply register since their construction is not actually enclosed by the
* block.
* <p>
* When the deferred registration occurs, it will NOT be global and will
* NOT be added to registered Commitables.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
* <p>
* The given object will only be added if it is not already in the list.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param target
* The object references to be registered. This cannot be
* <code>null</code>.
*/
private static void registerNext(WorkArea wa, LazyUndoable... target)
{
registerAt(wa, ScopeLevel.NEXT, target);
}
/**
* Adds the given object into the list of objects which require undo
* notifications for the next block that is opened.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block. In particular, it is necessary for
* objects that are constructed during a class' constructor/initializer
* (before the external procedure block has opened). Such objects cannot
* simply register since their construction is not actually enclosed by the
* block.
* <p>
* When the deferred registration occurs, it will NOT be global and will
* NOT be added to registered Commitables.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
* <p>
* The given object will only be added if it is not already in the list.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param scope
* The scope where to register the undoable targets.
* @param target
* The object references to be registered. This cannot be
* <code>null</code>.
*/
private static void registerAt(WorkArea wa, ScopeLevel scope, LazyUndoable... target)
{
if (wa.deferredUndoables == null)
{
wa.deferredUndoables = new IdentityHashMap<>();
}
// prevent ConcurrentModificationException
if (wa.nowIterating == wa.deferredUndoables)
{
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"registerNext",
"Illegal attempt to register Undoable",
new Throwable());
}
return;
}
if (target.length == 0)
{
if (wa.deferredDynExt == null)
{
wa.deferredDynExt = Collections.newSetFromMap(new IdentityHashMap<>());
}
wa.deferredDynExt.add((BaseDataType[]) target);
}
else
{
boolean global = (scope == ScopeLevel.GLOBAL);
for (LazyUndoable u : target)
{
wa.deferredUndoables.put(u, null);
if (wa.trackUndoables)
{
if (global)
{
u.setGlobal(true);
}
else
{
wa.trackedUndoables.put(u, null);
}
}
}
}
}
/**
* Register the specified targets as undoables. This will mark the object as undoable and save
* the current tx level, regardless if a tx is active or not.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param undoTargets
* List of object references to be registered. This cannot be
* <code>null</code>.
*/
private static void registerCurrent(WorkArea wa, LazyUndoable... undoTargets)
{
boolean persistent = wa.blocks.peek().external && wa.pm.isThisProcedurePersistent();
registerAt(wa, wa.txNestingLevel, persistent, undoTargets);
}
/**
* Adds the object into a scoped dictionary of objects requiring UNDO support. This registry
* is used to backup the objects at the beginning of transactions or sub-transactions.
* <p>
* These registrations naturally are removed when the associated scope is removed. However,
* the global scope is long-lived. Special care is necessary to prevent the same instance of
* global shared variables being added multiple times.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param transLevel
* The transaction level to set for this undoable.
* @param persistent
* Flag indicating if the undoable is defined in a persistent procedure
* @param undoTargets
* The object references to be rolled back on failure. This
* cannot be <code>null</code>.
*/
private static void registerAt(WorkArea wa,
int transLevel,
boolean persistent,
LazyUndoable... undoTargets)
{
registerAt(wa, transLevel, persistent, null, undoTargets);
}
/**
* Adds the object into a scoped dictionary of objects requiring UNDO support. This registry
* is used to backup the objects at the beginning of transactions or sub-transactions.
* <p>
* These registrations naturally are removed when the associated scope is removed. However,
* the global scope is long-lived. Special care is necessary to prevent the same instance of
* global shared variables being added multiple times.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param transLevel
* The transaction level to set for this undoable.
* @param persistent
* Flag indicating if the undoable is defined in a persistent procedure
* @param When not-null, persistent must be true and the referent represents the instance where to look
* for undoables. Otherwise, THIS-PROCEDURE will be used.
* @param undoTargets
* The object references to be rolled back on failure. This
* cannot be <code>null</code>.
*/
private static void registerAt(WorkArea wa,
int transLevel,
boolean persistent,
Object referent,
LazyUndoable... undoTargets)
{
for (LazyUndoable undoTarget : undoTargets)
{
registerUndo(undoTarget, transLevel, persistent);
}
if (persistent)
{
if (referent == null)
{
referent = wa.pm._thisProcedure();
}
IdentityHashMap<LazyUndoable, Object> pundoables = wa.procUndoables.get(referent);
if (pundoables == null)
{
wa.procUndoables.put(referent, pundoables = new IdentityHashMap<>());
}
// if this is an external block and this is a persistent procedure, then collect these
for (LazyUndoable target : undoTargets)
{
pundoables.put(target, null);
}
}
}
/**
* Enable tracking of all the undaobles defined in the next instantiating external program.
*
* @param wa
* Context local work area. Must not be {@code null}.
*/
private static void trackUndoables(WorkArea wa)
{
wa.trackUndoables = true;
wa.trackedUndoables = new IdentityHashMap<>();
}
/**
* Untrack (by removing from all the tx blocks) all the undoables defined and registered for
* the specified external program.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param referent
* The external program instance.
*/
private static void untrackUndoables(WorkArea wa, Object referent)
{
IdentityHashMap<LazyUndoable, Object> pundoables = wa.procUndoables.remove(referent);
if (pundoables == null)
{
return;
}
// this is called when a persistent procedure gets deleted - once that happens, there is nothing left
// in the procedure's state which can be undoable
deregister(wa, pundoables);
}
/**
* Rollback the transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param chained
* Flag indicating if a new transaction will be started.
*/
private static void rollbackTx(WorkArea wa, boolean chained)
{
BlockDefinition blk = wa.blocks.get(APPSERVER_ROOT_BLOCK_INDEX);
try
{
processRollback(wa, blk);
}
finally
{
endTx(wa, blk);
if (chained)
{
beginTx(wa, blk);
}
}
}
/**
* Commit the transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param chained
* Flag indicating if a new transaction will be started.
*/
private static void commitTx(WorkArea wa, boolean chained)
{
BlockDefinition blk = wa.blocks.get(APPSERVER_ROOT_BLOCK_INDEX);
try
{
processCommit(wa, blk);
}
finally
{
endTx(wa, blk);
if (chained)
{
beginTx(wa, blk);
}
}
}
/**
* Begin an explicit transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
* <p>
* This explicitly marks this tx as being open by the
* {@link TransactionResource#getTxInitProcedure() TRANS-INIT-PROCEDURE}.
*
* @param wa
* The current {@link WorkArea} instance. When <code>null</code>, resolve it.
* @param blk
* The {@link #APPSERVER_ROOT_BLOCK_INDEX root block}. When <code>null</code>,
* resolve it.
*/
private static void beginTx(WorkArea wa, BlockDefinition blk)
{
if (wa == null)
{
wa = workArea.obtain();
}
if (blk == null)
{
blk = wa.blocks.get(APPSERVER_ROOT_BLOCK_INDEX);
}
TransactionImpl.setOpenByTxInitProc(true);
blk.committed = false;
blk.rolledBack = false;
blk.full = true;
blk.level = TRANSACTION;
// there is only one level (the root one)
wa.setTransactionLevel(0);
// this is set only for the appserver target's block scope, but nothing else is done
wa.txNestingLevel = wa.blocks.peek().level > 0 ? 1 : 0;
blk.txNestingLevel = wa.txNestingLevel;
// we need to notify the BufferManager that a tx is active
BufferManager.get().beginTx(true, true, APPSERVER_ROOT_BLOCK_INDEX + 1);
}
/**
* End an explicit transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*
* @param wa
* The current {@link WorkArea} instance. When <code>null</code>, resolve it.
* @param blk
* The {@link #APPSERVER_ROOT_BLOCK_INDEX root block}. When <code>null</code>,
* resolve it.
*/
private static void endTx(WorkArea wa, BlockDefinition blk)
{
if (wa == null)
{
wa = workArea.obtain();
}
if (blk == null)
{
blk = wa.blocks.get(APPSERVER_ROOT_BLOCK_INDEX);
}
try
{
BufferManager bufMan = BufferManager.get();
bufMan.endTx(true, true);
bufMan.endTxPost();
}
finally
{
blk.full = false;
blk.level = NO_TRANSACTION;
blk.txNestingLevel = -1;
wa.setTransactionLevel(-1);
wa.txNestingLevel = -1;
TransactionImpl.setOpenByTxInitProc(false);
}
}
/**
* Worker that notifies all objects in the scopeable list that a block
* scope is being started or ended.
*
* @param wa
* Context-local work area.
* @param blk
* The scope to be finalized.
* @param start
* If <code>true</code> generate a start scope notification,
* otherwise generate an end scope notification.
*/
private static void processScopeNotifications(WorkArea wa, BlockDefinition blk, boolean start)
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "processScopeNotifications", "start = " + start);
}
if (wa.pendingScopeables != null)
{
for (int i = 0; i < wa.pendingScopeables.length; i++)
{
Scopeable target = wa.pendingScopeables[i];
if (target != null)
{
registerScopeable(wa, blk, target, false);
}
}
wa.pendingScopeables = null;
}
// check scopeables inherited from previous block
BlockDefinition prev = getBlock(wa, 1);
int accumMgrId = ScopeId.ACCUMULATOR_MANAGER.ordinal();
if (!blk.topLevel &&
prev != null &&
prev.scopeList != null &&
prev.scopeList[accumMgrId] != null &&
(blk.scopeList == null || blk.scopeList[accumMgrId] == null))
{
registerScopeable(wa, blk, AccumulatorManager.getInstance(), false);
}
Object prevReferent = wa.pm.getProcessedProcedure();
try
{
if (!(blk.topLevel || blk.scopeList != null))
{
// nothing to notify
return;
}
Object referent;
boolean delayed;
// can not call for start, as the procedure is not yet initialized
if (!start && blk.topLevel && !isGlobalBlock(wa))
{
referent = wa.pm._thisProcedure();
wa.pm.setProcessedProcedure(referent);
delayed = getNearestTopLevelType(wa) == EXTERNAL_PROC &&
wa.pm.isThisProcedurePersistent() &&
!wa.pm._isDelayedDelete(referent);
}
else
{
delayed = false;
referent = null;
}
if (start && blk.topLevel)
{
// procedure scopes need to be opened first
wa.procScopeable.scopeStart(blk);
}
if (blk.scopeList != null)
{
wa.nowIterating = blk.scopeList;
for (int i = 0; i < blk.scopeList.length; i++)
{
Scopeable target = blk.scopeList[i];
if (target == null)
{
continue;
}
try
{
if (start)
{
target.scopeStart(blk);
}
else
{
target.scopeFinished();
if (delayed)
{
wa.pm.addScopeable(referent, target);
}
else
{
target.scopeDeleted();
}
}
}
catch (Throwable thr)
{
deferError(wa, (start ? "scopeStart" : "scopeFinished"), thr, false);
}
}
}
if (!start && blk.topLevel)
{
// procedure scopes need to be finished last
wa.procScopeable.scopeFinished();
}
}
finally
{
wa.nowIterating = null;
wa.pm.setProcessedProcedure(prevReferent);
}
}
/**
* Walk through the current backup set and refresh the stored value for
* each one. This processing corresponds to the end of a sub-transaction
* (see {@link #blockSetup}).
*/
private static void updateBackupSet(List<UndoablePair> backup)
{
for (UndoablePair undoPair : backup)
{
if (undoPair.src.changed())
{
undoPair.copy = (LazyUndoable) undoPair.src.deepCopy();
}
}
}
/**
* Reports if the current block has the given property.
*
* @param wa
* Context local work area. Must not be {@code null}.
* @param prop
* The property bit flag to test. Should be one of the constants
* <code>PROP_ERROR, PROP_ENDKEY, PROP_STOP or PROP_QUIT</code>.
*
* @return <code>true</code> if the current block has the given
* property bit set in its properties bitmap.
*/
private static boolean hasProperty(WorkArea wa, int prop)
{
BlockDefinition blk = wa.blocks.peek();
return (blk.props & prop) != 0;
}
/**
* Write a formatted log entry with the error text and some state based
* on the current block.
*
* @param lvl
* The logging level.
* @param location
* The event or call site from which the log entry was
* generated.
* @param txt
* The text to insert into the log. May be <code>null</code>.
*/
private static void log(Level lvl,
String location,
String txt)
{
log(lvl, location, txt, null);
}
/**
* Write a formatted log entry with the error text and some state based
* on the current block.
*
* @param lvl
* The logging level.
* @param location
* The event or call site from which the log entry was
* generated.
* @param txt
* The text to insert into the log. May be <code>null</code>.
* @param trw
* The throwable that caused this logging to occur or
* <code>null</code> if no exception is present.
*/
private static void log(Level lvl,
String location,
String txt,
Throwable trw)
{
if (LOG.isLoggable(lvl))
{
WorkArea wa = workArea.obtain();
BlockDefinition blk = null;
if (wa.depth() > 0)
{
blk = wa.blocks.peek();
}
String err = CentralLogger.generate("%s %s %s",
wa.toString(),
(blk == null ? "" : blk.toString()),
(txt == null ? "" : txt));
if (trw == null)
{
LOG.logp(lvl, "TransactionManager." + location, "", err);
}
else
{
LOG.logp(lvl, "TransactionManager." + location, "", err, trw);
}
}
}
/**
* Helper to expose transaction state in a way that avoids context local lookups.
*/
public static class TransactionHelper
{
/** Quick access to context-local error helper. */
public final ErrorManager.ErrorHelper errHlp;
/** The {@link WorkArea} instance. */
private final WorkArea wa;
/**
* Create a new instance and associate the given WorkArea instance.
*
* @param wa
* The {@link WorkArea} instance.
*/
public TransactionHelper(WorkArea wa)
{
this.wa = wa;
this.errHlp = wa.em;
}
/**
* Get the current {@link WorkArea#nowIterating} instance.
*
* @return See above.
*/
public Object getNowIterating()
{
return wa.nowIterating;
}
/**
* Set the current {@link WorkArea#nowIterating} instance.
*
* @param nowIterating
* The instance (saved via {@link #getNowIterating()} or <code>null</code>) to restore.
*/
public void setNowIterating(Object nowIterating)
{
wa.nowIterating = nowIterating;
}
/**
* Get the {@link WorkArea#pendingScopeables} list.
*
* @return See above.
*/
public Scopeable[] getPendingScopeables()
{
return wa.pendingScopeables == null
? null
: Arrays.copyOf(wa.pendingScopeables, wa.pendingScopeables.length);
}
/**
* Set the {@link WorkArea#pendingScopeables}.
*
* @param scopeables
* The pending scopeables.
*/
public void setPendingScopeables(Scopeable[] scopeables)
{
if (scopeables == null)
{
wa.pendingScopeables = null;
return;
}
if (wa.pendingScopeables == null)
{
wa.pendingScopeables = new Scopeable[ScopeId.values().length];
}
for (int i = 0; i < scopeables.length; i++)
{
wa.pendingScopeables[i] = scopeables[i];
}
}
/**
* Check whether the transaction id should be set for a database.
*
* @param db
* the logical name of the database (The {@code getId()} of the {@code Database}).
*/
public void checkTransaction(String db)
{
if (!wa.isTransaction())
{
// no transaction active? return unknown value
return;
}
SecurityManager sm = SecurityManager.getInstance();
if (sm == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Failed to obtain the session Id");
}
return;
}
Integer userId = sm.sessionSm.getSessionId();
if (userId == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Failed to obtain the session Id");
}
return;
}
DbTransactions dbData;
synchronized (transactionData)
{
dbData = transactionData.get(db);
if (dbData == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Database " + db +
" was not found in the list of active databases. Adding it now.");
}
// first access to this database?
dbData = new DbTransactions();
transactionData.put(db, dbData);
if (TransactionTableUpdater.isEnabled())
{
updaters.put(db, new TransactionTableUpdater());
}
}
}
synchronized (dbData)
{
// if transaction for this used has not been set, increment and add now
if (!dbData.transactions.containsKey(userId))
{
int tId = ++dbData.lastTransactionId;
if (tId < 0) // wrapping around from 1 , not Integer.MIN_VALUE
{
tId = 1;
}
dbData.transactions.put(userId, tId);
dbData.isDirty = true;
}
}
}
/**
* Reports on the current <code>Finalizable</code> operation being
* executed.
*
* @return One of the following:
* <ul>
* <li> <code>OP_NONE</code>
* <li> <code>OP_ITERATE</code>
* <li> <code>OP_RETRY</code>
* <li> <code>OP_FINISHED</code>
* </ul>
*/
public int currentOperation()
{
return wa.op;
}
/**
* Register StopAfterTimer to a ContextLocal WorkArea.
*
* @param stopAfterTimer
* StopAfterTimer to register.
*/
public void registerStopAfterTimer(StopAfterTimer stopAfterTimer)
{
TransactionManager.registerStopAfterTimer(wa, stopAfterTimer);
}
/**
* Adds the given object into the list of objects for the current block
* which require commit notification. This form of registration is
* useful for objects whose natural lifetime are scoped to a different
* block than the block in which they must be committed. For example,
* if a global or shared resource needs to be committed in a specific
* scope, then this can be used in cooperation with the normal
* {@link #registerCurrent} processing to control the commit points.
* <p>
* This notification will ONLY occur when there is an active transaction.
* <p>
* Notifications occur in LIFO order; thus, commitables which depend upon
* others being committed first must be registered before the objects upon
* which they are dependent.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
*
* @param target
* The object reference to be committed on success. This
* cannot be <code>null</code>.
*/
public void registerCommit(Commitable target)
{
TransactionManager.registerCommitAt(wa, wa.blocks.size() - 1, target);
}
/**
* Adds the given object into the list of objects which require commit notification for the
* block at the specified depth This form of registration is useful for objects whose natural
* lifetime are scoped to a different block than the block in which they must be committed.
* For example, if a global or shared resource needs to be committed in a specific scope,
* then this can be used in cooperation with the normal {@link #registerCurrent} processing to
* control the commit points.
* <p>
* This notification will ONLY occur when there is an active transaction.
* <p>
* Notifications occur in LIFO order; thus, commitables which depend upon
* others being committed first must be registered before the objects upon
* which they are dependent.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
*
* @param blockDepth
* Zero-based depth of the target block, starting from the outermost block.
* @param target
* The object reference to be committed on success. This cannot be
* <code>null</code>.
*/
public void registerCommitAt(int blockDepth, Commitable target)
{
TransactionManager.registerCommitAt(wa, blockDepth, target);
}
/**
* Adds the given object as the transaction level reference which will get
* a single, "master" commit call after all other commits have been
* processed at the full transaction level. This will never be called
* for sub-transaction commits, nor will it be called if the transaction
* is abnormally exited.
* <p>
* This notification will ONLY occur when there is an active transaction
* and it only will be called at the exit of the block that corresponds
* to the full transaction.
* <p>
* If the full transaction block is also an iterating block, this
* notification will occur for each iteration of the block.
*
* @param target
* The object reference to be committed on success. This
* can be <code>null</code> if one intends to clear the current
* reference.
* @param remove
* If <code>true</code> automatically remove this registration
* after the full transaction scope has ended (whether or not
* the commit occurs). If <code>false</code>, this reference
* will remain globally until replaced.
*/
public void registerTransactionCommit(Commitable target, boolean remove)
{
if (wa.masterCommit == null)
{
wa.masterCommit = new LinkedHashMap<>();
}
wa.masterCommit.put(target, remove ? Boolean.TRUE : Boolean.FALSE);
}
/**
* Register the given object as a handler which can veto the processing of
* STOP condition processing at the current scope. Handlers are invoked
* in FIFO order. Any handler can issue a veto, which forces the STOP
* condition to be handled at a higher scope, if at all.
*
* @param h
* The stop condition veto handler.
*/
public void registerStopVetoHandler(StopConditionVetoHandler h)
{
wa.stopHandlers.add(h);
}
/**
* Registers the instance for scope notifications at the current block.
* <p>
* This will call {@link Scopeable#scopeStart} to notify for the scope processing.
*
* @param target
* The scopeable instance.
*/
public void registerBlockScopeable(Scopeable target)
{
TransactionManager.registerBlockScopeable(wa, target);
}
/**
* Register the target scopeable as pending, to be added to the next block.
*
* @param target
* The scopeable instance.
*/
public void registerPendingScopeable(Scopeable target)
{
TransactionManager.registerPendingScopeable(wa, target);
}
/**
* Registers the instance for scope notifications at the nearest {@link #findNearestTopLevel() top level}
* block.
* <p>
* WARNING: this must be called only for instances which require notifications only for
* top-level blocks, as this will registered the instance at the nearest top-level block and call
* {@link Scopeable#scopeStart} to emulate as if it was called when the top-level block started.
*
* @param target
* The scopeable instance.
*/
public void registerTopLevelScopeable(Scopeable target)
{
BlockDefinition block = TransactionManager.nearestTopLevel(wa);
registerScopeable(wa, block, target, true);
}
/**
* Search the stack of scopes and return the first top level block found.
*
* @return The nearest enclosing top level block or <code>null</code> if there is no block marked as a
* top level block.
*/
public BlockDefinition nearestTopLevel()
{
return TransactionManager.nearestTopLevel(wa);
}
/**
* Search the stack of scopes and return the previous top level block.
*
* @return The previous enclosing top level block or <code>null</code> if there is no block marked as
* a top level block after the nearest top-level block.
*/
public BlockDefinition prevTopLevel()
{
Stack<BlockDefinition> blocks = wa.blocks;
boolean found = false;
for (int idx = blocks.size() - 1; idx >= 0; idx--)
{
BlockDefinition blk = blocks.get(idx);
if (blk.topLevel)
{
if (found)
{
return blk;
}
found = true;
}
}
return null;
}
/**
* Register the given <code>Resettable</code> object in order to reset it
* at specific points of block processing.
*
* @param resettable
* <code>Resettable</code> to be registered.
*/
public void registerResettable(Resettable resettable)
{
wa.resettables.add(resettable);
}
/**
* Adds the given object into the list of objects for the specified block
* which require iterate/retry/finish notification. This form of
* registration is useful for objects whose natural lifetime is scoped to
* a block and which need a hook for cleaning up resources or handling
* other block termination processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called)
* in the case of a non-global scope OR at the moment that the last
* scope has been removed in the case of the global scope.
* <p>
* The given object will only be added if it is not already in the list.
* <p>
* Finalizables for a scope are processed in the same order as they were
* registered.
*
* @param target
* The object reference to be notified on any exit from the
* block specified. This cannot be <code>null</code>.
* @param global
* Specifies the target scope. <code>false</code> specifies the
* current scope, and <code>true</code> specifies the global
* scope.
*/
public void registerFinalizable(Finalizable target, boolean global)
{
int blockDepth = global ? 0 : wa.blocks.size() - 1;
TransactionManager.registerFinalizableAt(wa, blockDepth, target);
}
/**
* Remove the given finalizable from the global list of finalizables.
*
* @param target
* Finalizable to be removed from the global list.
*/
public void deregisterGlobalFinalizable(Finalizable target)
{
TransactionManager.deregisterGlobalFinalizable(wa, target);
}
/**
* Adds the given object into the list of objects for the specified block
* which require iterate/retry/finish notification. This form of
* registration is useful for objects whose natural lifetime is scoped to
* a block and which need a hook for cleaning up resources or handling
* other block termination processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called)
* in the case of a non-global scope OR at the moment that the last
* scope has been removed in the case of the global scope.
* <p>
* The given object will only be added if it is not already in the list.
* <p>
* Finalizables for a scope are processed in the same order as they were
* registered.
*
* @param blockDepth
* zero-based depth of target block, starting from the outermost
* block. <code>0</code> indicates the global block.
* @param target
* The object reference to be notified on any exit from the
* block specified. This cannot be <code>null</code>.
*/
public void registerFinalizableAt(int blockDepth, Finalizable target)
{
TransactionManager.registerFinalizableAt(wa, blockDepth, target);
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notifications for the next block that is opened
* which is a top-level block that corresponds to a Progress external
* procedure.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block and which need a hook for cleaning up
* resources or handling other block termination processing. In
* particular, it is necessary for objects that are constructed during
* a class' constructor/initializer (before the external procedure block
* has opened). Such objects cannot simply register since their
* construction is not actually enclosed by the block.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
*
* @param target
* The object reference to be notified on any exit from the
* top-level external block. This cannot be <code>null</code>.
*
* @throws IllegalStateException
* if the top-level block cannot be identified.
*/
public void registerNextExternal(Finalizable target)
{
TransactionManager.registerNextExternal(wa, target);
}
/**
* Allow finalizables to do their cleanup ({@link Finalizable#initFailure()}) now since the
* scoped they were designed was aborted and there won't be other chance to call their
* {@link Finalizable#finished()}.
* <p>
* After iteration the list is deleted.
* <p>
* Also, cleanup all other pending state being collected while the external program was
* instantiating.
*/
public void cleanupPending()
{
TransactionManager.cleanupPending(wa);
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notifications for the nearest enclosing
* top-level block. A top-level block is generated for Progress external
* procedures, internal procedures, triggers and functions. An additional
* parameter allows registration to be specified for the specific block
* which corresponds with an external procedure block scope in Progress.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block and which need a hook for cleaning up
* resources or handling other block termination processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
* <p>
* Finalizables for a scope are processed in the same order as they were
* registered.
*
* @param target
* The object reference to be notified on any exit from the
* top-level block. This cannot be <code>null</code>.
* @param external
* If <code>true</code>, register with the nearest enclosing
* external block (external procedure), otherwise register with
* the nearest enclosing top-level block (which may be but is
* not guaranteed to be the external block).
*
* @throws IllegalStateException
* if the top-level block cannot be identified.
*/
public void registerTopLevelFinalizable(Finalizable target, boolean external)
{
TransactionManager.registerTopLevelFinalizable(wa, target, external);
}
/**
* Adds the given object into the list of objects which require
* iterate/retry/finish notification for the nearest enclosing, looping
* block.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the nearest enclosing, looping block and which need a hook
* for cleaning up resources or handling other block termination
* processing.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed (after the {@link Finalizable#finished} method is called).
* <p>
* The given object will only be added if it is not already in the list.
*
* @param target
* The object reference to be notified on any exit from or
* iteration of the target block. This cannot be
* <code>null</code>.
*
* @throws IllegalStateException
* if no enclosing, looping block can be identified.
*/
public void registerNearestLoopFinalizable(Finalizable target)
{
TransactionManager.registerNearestLoopFinalizable(wa, target);
}
/**
* Removes the given object from the list of objects which require
* iterate/retry/finish notifications. This will find the first instance
* of the given reference by walking up each block from the current to the
* global block. The first instance found will be removed and the method
* will then return. If the same instance is registered in multiple scopes,
* it would take multiple calls to remove all instances.
* <p>
* After this call, no notifications will be received.
*
* @param target
* The object reference to be removed.
*
* @return <code>true</code> if the target instance was found and
* removed.
*/
public boolean removeFinalizable(Finalizable target)
{
if (target == null)
{
// nothing to do
return false;
}
return TransactionManager.removeFinalizable(wa, target);
}
/**
* Adds the given object as the transaction level reference which will get
* a "master" finish call after all other finish notifications have been
* processed at the full transaction level. This will never be called for
* sub-transaction finish notification.
* <p>
* This notification will ONLY occur when there is an active transaction
* and it only will be called at the exit of the block that corresponds
* to the full transaction.
* <p>
* Multiple targets may be registered by invoking this method multiple
* times. However, the same object registered multiple times will receive
* the notification only once per full transaction block exit. If multple
* targets are registered, they will be called in the order in which they
* were registered.
*
* @param target
* The object reference to be finished at full transaction block
* end.
* @param remove
* If <code>true</code> automatically remove this registration
* after the full transaction scope has ended. If
* <code>false</code>, this reference will remain globally until
* replaced.
*/
public void registerTransactionFinish(Finalizable target, boolean remove)
{
if (wa.masterFinish == null)
{
wa.masterFinish = new LinkedHashMap<>();
}
wa.masterFinish.put(target, remove);
}
/**
* Adds the given object into the batch notifier list. This is a global
* list (it isn't associated with any block) that is triggered externally
* by a call to {@link #batchStart} and {@link #batchStop}. Every object
* in the list will be notified in the order in which they were added to
* the list.
*
* @param target
* The object reference to be notified of any batch start and
* stop.
*/
public void registerBatchListener(BatchListener target)
{
wa.batchListeners.add(target);
}
/**
* Reports whether the current block was just rolled back or a containing
* block has a pending rollback. If there is no current transaction open,
* then there cannot be a pending or current rollback.
*
* @return <code>true</code> if there was or will be a rollback.
*/
public boolean hadRollback()
{
return TransactionManager.hadRollback(wa);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the scope defined by the
* current block. All objects that are rolled back will have their
* values assigned based on the values backed up at the start of the
* specified scope. A flow control change MUST always immediately follow
* this method otherwise undefined results can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*/
public void rollback()
{
rollback(null);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the scope defined
* explicitly by the given label or implicitly if the label is
* <code>null</code>. All objects that are rolled back will have their
* values assigned based on the values backed up at the start of the
* specified scope. If the current scope is not the target scope, the
* given exception will be stored, the rollback will be deferred and
* the rollback will be processed as the scopes naturally unwind. This
* unwinding must be generated by a change in flow of control by the
* calling code. This flow control change MUST always immediately follow
* this method otherwise undefined results can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*
* @param label
* Specifies the name of the scope at which to rollback or
* <code>null</code> if the scope should be implicitly
* determined.
*/
public void rollback(String label)
{
rollback(label, null);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the scope defined
* explicitly by the given label or implicitly if the label is
* <code>null</code>. All objects that are rolled back will have their
* values assigned based on the values backed up at the start of the
* specified scope. If the current scope is not the target scope, the
* given exception will be stored, the rollback will be deferred and
* the rollback will be processed as the scopes naturally unwind. This
* unwinding must be generated by a change in flow of control by the
* calling code. This flow control change MUST always immediately follow
* this method otherwise undefined results can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*
* @param label
* Specifies the name of the scope at which to rollback or
* <code>null</code> if the scope should be implicitly
* determined.
* @param cause
* The exceptional cause, if any, of the rollback.
*/
public void rollback(String label, Throwable cause)
{
TransactionManager.rollback(wa, label, cause);
}
/**
* Trigger a rollback for all registered <code>Undoable</code> objects
* inside the current active transaction at the nearest top level scope.
* All objects that are rolled back will have their values assigned based
* on the values backed up at the start of the specified scope. If the
* current scope is not the target scope, the given exception will be
* stored, the rollback will be deferred and the rollback will be processed
* as the scopes naturally unwind. This unwinding must be generated by a
* change in flow of control by the calling code. This flow control change
* MUST always immediately follow this method otherwise undefined results
* can occur.
* <p>
* This caller-driven control flow is required to properly duplicate the
* semantics of Progress 4GL condition processing which always generates
* an implicit or explicit flow control action after every UNDO. Since the
* flow of control change can reference a different scope than that of
* the rollback, this can't be handled by re-throwing the exception but
* must instead be separately driven by a deferred rollback and a caller
* driven flow control statement such as break, continue or return. The
* key limitation that makes this possible is the fact that the UNDO
* (rollback) scope MUST ALWAYS BE equivalent to or more specific than
* the scope to which flow of control will be changed. This means that
* one can rollback at a more deeply nested scope but change flow of
* control to resume processing at a different (less deeply nested) scope.
* The reverse is not possible.
*/
public void rollbackTopLevel()
{
TransactionManager.rollbackTopLevel(wa);
}
/**
* Initializes a new scope, the associated block definition data
* structures. A backup of objects that can be rolled back is deferred
* until after the block opens (see {@link #blockSetup} and
* {@link #makeBackup}).
* <p>
* In the case of a block which is defined as <code>NO_TRANSACTION</code>,
* the core data structures are initialized but no rollback support will
* be provided.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}.
* <p>
* This will create a block of <code>BlockType.UNKNOWN</code> and block
* properties <code>PROP_NONE</code>.
*
* @param label
* The text label associated with the block. This usually
* should correspond with the label used in the source file
* to identify the block for purposes of the break and continue
* statements.
* @param level
* One of {@link #NO_TRANSACTION}, {@link #SUB_TRANSACTION} or
* {@link #TRANSACTION}. This code defines the level of
* rollback and transaction support which should be provided.
* @param external
* <code>true</code> if this block is associated with the
* external procedure (in Progress terms). This controls the
* cleanup of certain resources such as temp-tables, frames
* and streams.
* @param topLevel
* <code>true</code> if this block is a method.
* @param loop
* Defines if this block is a loop or not.
* @param next
* <code>true</code> if this infinite loop protection should
* convert a retry into a next (continue), otherwise the retry
* will be converted into a leave (break).
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public void pushScope(String label,
int level,
boolean external,
boolean topLevel,
boolean loop,
boolean next)
{
pushScope(label, level, PROP_NONE, external, topLevel, loop, next, UNKNOWN);
}
/**
* Initializes a new scope, the associated block definition data
* structures. A backup of objects that can be rolled back is deferred
* until after the block opens (see {@link #blockSetup} and
* {@link #makeBackup}).
* <p>
* In the case of a block which is defined as <code>NO_TRANSACTION</code>,
* the core data structures are initialized but no rollback support will
* be provided.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}.
* <p>
* This will create a block with block properties <code>PROP_NONE</code>.
*
* @param label
* The text label associated with the block. This usually
* should correspond with the label used in the source file
* to identify the block for purposes of the break and continue
* statements.
* @param level
* One of {@link #NO_TRANSACTION}, {@link #SUB_TRANSACTION} or
* {@link #TRANSACTION}. This code defines the level of
* rollback and transaction support which should be provided.
* @param external
* <code>true</code> if this block is associated with the
* external procedure (in Progress terms). This controls the
* cleanup of certain resources such as temp-tables, frames
* and streams.
* @param topLevel
* <code>true</code> if this block is a method.
* @param loop
* Defines if this block is a loop or not.
* @param next
* <code>true</code> if this infinite loop protection should
* convert a retry into a next (continue), otherwise the retry
* will be converted into a leave (break).
* @param blockType
* Specifies this block's type.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public void pushScope(String label,
int level,
boolean external,
boolean topLevel,
boolean loop,
boolean next,
BlockType blockType)
{
pushScope(label, level, PROP_NONE, external, topLevel, loop, next, UNKNOWN);
}
/**
* Initializes a new scope, the associated block definition data
* structures. A backup of objects that can be rolled back is deferred
* until after the block opens (see {@link #blockSetup} and
* {@link #makeBackup}).
* <p>
* In the case of a block which is defined as <code>NO_TRANSACTION</code>,
* the core data structures are initialized but no rollback support will
* be provided.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @param label
* The text label associated with the block. This usually
* should correspond with the label used in the source file
* to identify the block for purposes of the break and continue
* statements.
* @param level
* One of {@link #NO_TRANSACTION}, {@link #SUB_TRANSACTION} or
* {@link #TRANSACTION}. This code defines the level of
* rollback and transaction support which should be provided.
* @param props
* A bitfield storing block properties which will be a
* combination of {@link #PROP_NONE}, {@link #PROP_ERROR},
* {@link #PROP_ENDKEY}, {@link #PROP_STOP} or
* {@link #PROP_QUIT}.
* @param external
* <code>true</code> if this block is associated with the
* external procedure (in Progress terms). This controls the
* cleanup of certain resources such as temp-tables, frames
* and streams.
* @param topLevel
* <code>true</code> if this block is a method.
* @param loop
* Defines if this block is a loop or not.
* @param next
* <code>true</code> if this infinite loop protection should
* convert a retry into a next (continue), otherwise the retry
* will be converted into a leave (break).
* @param blockType
* Specifies this block's type.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public void pushScope(String label,
int level,
int props,
boolean external,
boolean topLevel,
boolean loop,
boolean next,
BlockType blockType)
{
pushScope(label, level, props, external, topLevel, loop, next, false, blockType);
}
/**
* Initializes a new scope, the associated block definition data
* structures. A backup of objects that can be rolled back is deferred
* until after the block opens (see {@link #blockSetup} and
* {@link #makeBackup}).
* <p>
* In the case of a block which is defined as <code>NO_TRANSACTION</code>,
* the core data structures are initialized but no rollback support will
* be provided.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @param label
* The text label associated with the block. This usually
* should correspond with the label used in the source file
* to identify the block for purposes of the break and continue
* statements.
* @param level
* One of {@link #NO_TRANSACTION}, {@link #SUB_TRANSACTION} or
* {@link #TRANSACTION}. This code defines the level of
* rollback and transaction support which should be provided.
* @param props
* A bitfield storing block properties which will be a
* combination of {@link #PROP_NONE}, {@link #PROP_ERROR},
* {@link #PROP_ENDKEY}, {@link #PROP_STOP} or
* {@link #PROP_QUIT}.
* @param external
* <code>true</code> if this block is associated with the
* external procedure (in Progress terms). This controls the
* cleanup of certain resources such as temp-tables, frames
* and streams.
* @param topLevel
* <code>true</code> if this block is a method.
* @param loop
* Defines if this block is a loop or not.
* @param next
* <code>true</code> if this infinite loop protection should
* convert a retry into a next (continue), otherwise the retry
* will be converted into a leave (break).
* @param databaseTrigger
* <code>true</code> if this scope should create a definition
* of a database trigger block.
* @param blockType
* Specifies this block's type.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public void pushScope(String label,
int level,
int props,
boolean external,
boolean topLevel,
boolean loop,
boolean next,
boolean databaseTrigger,
BlockType blockType)
{
TransactionManager.pushScope(wa,
label,
level,
props,
external,
topLevel,
loop,
next,
databaseTrigger,
blockType);
}
/**
* Remove the current block from the stack of scopes, process any pending
* rollbacks (if the current block is the proper target), notify any
* objects needing commits (if the block finished successfully) and handle
* finish processing for the block. This method must be called from a
* <code>finally</code> block such that this is guaranteed to be called
* no matter how the flow of control exits the associated block (exception,
* break, return or the flow of control naturally passing through the
* end of the block).
* <p>
* Rollback and commit processing only occur when there is an active
* transaction. Otherwise this processing is bypassed. Finish processing
* is always called.
* <p>
* This method implements global finish processing (as the last action
* before returning) when the last scope is popped from the stack of
* scopes. For this reason, the current implementation assumes that the
* session is over at the moment that the top-most user-defined scope
* exits.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public void popScope()
throws StopConditionException
{
TransactionManager.popScope(wa);
}
/**
* Notify {@link ConditionListener} instances about condition.
*
* @param condition
* The condition the listeners will be notified.
*/
public void notifyCondition(BlockManager.Condition condition)
{
// do not pass null down
if (condition == null)
{
return;
}
TransactionManager.notifyCondition(wa, condition);
}
/**
* Notify {@link ConditionListener} instances about exception condition.
*
* @param condition
* The instance of {@link ConditionException}.
*/
public void notifyCondition(ConditionException condition)
{
TransactionManager.notifyCondition(wa, BlockManager.exceptionToCondition(condition));
}
/**
* Check if the latest block has initialized.
*
* @return See above.
*/
public boolean blockInitialized()
{
BlockDefinition blk = wa.blocks.peek();
return blk.init;
}
/**
* Provides a callback entry point for the top of the loop. This location
* is required to handle the backup of undo variables via
* {@link #makeBackup}. On the second and subsequent executions of this
* method for a given block, this method handles <code>iterate</code>
* notifications.
* <p>
* It is important to note that <code>continue</code> can be called from
* nested blocks at arbitrary depths. This method can handle such nesting
* at any level.
* <p>
* This method will honor any pending thread interruption by throwing
* a {@link StopConditionException} using {@link #honorStopCondition}
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
*/
public void blockSetup()
throws StopConditionException
{
TransactionManager.blockSetup(wa);
}
/**
* Save the given runnable as the FINALLY block for the next iterate, retry or finished. This
* will be cleared after it is executed, so it must be setup each iteration of the block.
*
* @param fini
* The FINALLY block code to execute.
*/
public void setupFinallyBlock(Runnable fini)
{
BlockDefinition blk = wa.blocks.peek();
blk.finallyBlock = fini;
}
/**
* Save the given runnable as the CATCH block for the next iterate, retry or finished. This will be
* be cleared after it is executed, so it must be setup each iteration of the block.
*
* @param possibleCatch
* The CATCH block code to execute.
*/
public void setupCatchProcessing(Runnable possibleCatch)
{
BlockDefinition blk = wa.blocks.peek();
blk.possibleCatch = possibleCatch;
}
/**
* Save the given LegacyStopException. This will be cleared after it is executed,
* so it must be setup each iteration of the block.
*
* @param stopCondition
* LegacyStopException from stop-after to propagate.
*/
public void setupStopAfterProccessing(LegacyStopException stopCondition)
{
wa.stopAfterException = stopCondition;
}
/**
* Creates or updates the backup set for all objects needing rollback
* support in the current block, if a transaction is active. This is
* split up from {@link #pushScope} and {@link #blockSetup} due to the
* semantics of Progress in which the backup set is created or refreshed
* after the loop control variables have been modified.
*/
public void makeBackup()
{
TransactionManager.backupWorker(wa, null);
}
/**
* Reports if there is a current transaction which is active.
*
* @return <code>true</code> if a transaction is active.
*/
public logical isTransactionActive()
{
return new logical(wa.isTransaction());
}
/**
* Reports if the current iteration of this block or loop is a retry.
*
* @return <code>true</code> if this is a retry of this block or loop
* (as opposed to the first time through this iteration).
*/
public boolean _isRetry()
{
return TransactionManager._isRetry(wa);
}
/**
* Reports if the current iteration of this block or loop is a retry.
*
* @return <code>true</code> if this is a retry of this block or loop
* (as opposed to the first time through this iteration).
*/
public logical isRetry()
{
return new logical(_isRetry());
}
/**
* Records the fact that the pending RETRY is being caused by an ENDKEY
* condition. This enables "downstream" retry processing to implement the
* "stuttering" quirk and some behavior in honoring PAUSE as a way to
* disable infinite loop protection.
*/
public void markEndkeyRetry()
{
TransactionManager.markEndkeyRetry(wa);
}
/**
* This checks if any of the enclosing loops has iterated at least once.
*
* @return <code>true</code> if any of the enclosing loops has iterated
* at least once.
*/
public boolean hasParentIteratedBlock()
{
return TransactionManager.hasParentIteratedBlock(wa);
}
/**
* Determines if a NEXT operation with the given target block should
* be honored or if the NEXT should be converted to a LEAVE. This is
* a special form of infinite loop protection.
* <p>
* A NEXT is converted into a LEAVE when all of the following conditions
* are true:
* <p>
* <ul>
* <li> The NEXT is associated with the UNDO, NEXT language statement or
* with an ON phrase that has UNDO, NEXT as the result of the given
* condition (e.g. ON ENDKEY UNDO, NEXT). It is important to note
* that Progress does not provide infinite loop protection for the
* NEXT language statement, so there is no conversion to LEAVE in
* that case.
* <li> The target block (whether implicitly determined by the lack of a
* label or explicitly determined by the presence of a label) must
* also have been the target block for the UNDO operation. It is
* possible for the NEXT to be targetted at a more enclosing block
* than the UNDO, in which case there is no conversion to LEAVE.
* <li> The target block type must be REPEAT, REPEAT WHILE or DO WHILE.
* These are the only loop types that normally convert RETRY to
* LEAVE instead of NEXT. Likewise, these are the only loop types
* which honor infinite loop protection for NEXT by converting it
* to a LEAVE.
* <li> Infinite loop protection must not have been disabled for the
* target block. In this case, any form of PAUSE is honored as
* one of the mechanisms to disable infinite loop protection.
* </ul>
* <p>
* Since NEXT is only possible on a looping block, if the target block
* is not a looping block then this method will return <code>false</code>.
*
* @param label
* The label of the target block for the NEXT action.
*
* @return <code>true</code> for NEXT, <code>false</code> for LEAVE.
*/
public boolean honorNext(String label)
{
return TransactionManager.honorNext(wa, label);
}
/**
* Determines if the currently executing block should be retried. This
* is where infinite loop protection is implemented if it is enabled.
*
* @return <code>true</code> if the block (whether or not it is a loop)
* should be retried.
*/
public boolean needsRetry()
{
return TransactionManager.needsRetry(wa);
}
/**
* Accesses the state of whether there is a pending break that must be
* honored. This would have been generated by the infinite loop
* protection.
*
* @return <code>true</code> if a break is pending for this block.
*/
public boolean isBreakPending()
{
return TransactionManager.isBreakPending(wa);
}
/**
* Force an error to be generated such that it will be caught in the
* caller of this method. The stack will be unwound and normal error
* processing bypassed until this method returns. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method,
* the "normal" stack unwinding approach cannot be used because the
* exception cannot be thrown. Instead, the error manager will be updated
* with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a
* <code>return</code> statement which will maintain the proper Progress
* behavior. The caller can check the error manager to determine that an
* error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top
* level block be found. The silent error flag of the caller is cached
* there.
*
* @param text
* The text to use as the error message in the exception.
*
* @throws ErrorConditionException
*/
public void triggerErrorInCaller(String text)
throws ErrorConditionException
{
TransactionManager.triggerErrorInCaller(wa, null, text);
}
/**
* Check if this block's caller requires errors to be suppressed.
*
* @return {@link BlockDefinition#suppressError} for {@link #nearestTopLevel} block.
*/
public boolean isSuppressError()
{
return TransactionManager.isSuppressError(wa);
}
/**
* Force an error to be generated such that it will be caught in the
* caller of this method. The stack will be unwound and normal error
* processing bypassed until this method returns. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method,
* the "normal" stack unwinding approach cannot be used because the
* exception cannot be thrown. Instead, the error manager will be updated
* with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a
* <code>return</code> statement which will maintain the proper Progress
* behavior. The caller can check the error manager to determine that an
* error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top
* level block be found. The silent error flag of the caller is cached
* there.
*
* @throws ErrorConditionException
*/
public void triggerErrorInCaller()
throws ErrorConditionException
{
TransactionManager.triggerErrorInCaller(wa, null, "");
}
/**
* Force an error to be generated such that it will be caught in the
* caller of this method. The stack will be unwound and normal error
* processing bypassed until this method returns. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorError}.
* <p>
* In the case where silent error mode is enabled in the calling method,
* the "normal" stack unwinding approach cannot be used because the
* exception cannot be thrown. Instead, the error manager will be updated
* with the error data and this method will silently return. It is
* critical that in such cases the calling code must immediately execute a
* <code>return</code> statement which will maintain the proper Progress
* behavior. The caller can check the error manager to determine that an
* error occurred.
* <p>
* Detecting the silent error mode in the caller requires that the top
* level block be found. The silent error flag of the caller is cached
* there.
*
* @param lex
* The legacy OO-like error to throw.
*
* @throws ErrorConditionException
*/
public void triggerErrorInCaller(LegacyErrorException lex)
throws ErrorConditionException
{
TransactionManager.triggerErrorInCaller(wa, lex);
}
/**
* Force a retry of the current block (if there is no pending rollback)
* or retry the same block as the pending rollback, in either case
* ensure that the flow of control will not naturally continue.
* <p>
* A {@link RetryUnwindException} will be thrown to unwind the stack
* to the given scope, even if the scope is the current block. This
* unwinding is dependent upon catch blocks being implemented with a
* corresponding call to {@link #honorRetry}.
* <p>
* <b>This method will NEVER naturally return since an exception is ALWAYS
* thrown.</b> This method should generally not be called from within a
* catch block if the intention is to retry the current block since it
* would be outside of the block's associated <code>try</code>. In that
* case, only an enclosing block could be retried.
*
* @throws RetryUnwindException
* This method always generates this exception to ensure that
* natural processing will not continue.
*/
public void forceRetry()
throws RetryUnwindException
{
TransactionManager.forceRetry(wa, null);
}
/**
* Force a retry of the current block or the scope named by the given
* label, ensuring that the flow of control will not naturally continue.
* A {@link RetryUnwindException} will be thrown to unwind the stack
* to the given scope, even if the scope is the current block. This
* unwinding is dependent upon catch blocks being implemented with a
* corresponding call to {@link #honorRetry}.
* <p>
* <b>This method will NEVER naturally return since an exception is ALWAYS
* thrown.</b> This method should generally not be called from within a
* catch block if the intention is to retry the current block since it
* would be outside of the block's associated <code>try</code>. In that
* case, only an enclosing block could be retried.
*
* @param label
* The name of the scope to retry. Specify <code>null</code> to
* retry the scope that has a pending rollback OR to retry
* the current scope if there is no pending rollback.
*
* @throws RetryUnwindException
* This method always generates this exception to ensure that
* natural processing will not continue.
*/
public void forceRetry(String label)
throws RetryUnwindException
{
TransactionManager.forceRetry(wa, label);
}
/**
* Force a retry of the current block (if there is no pending rollback)
* or the block targeted for a pending rollback. If a containing scope is
* the target of the retry, then a {@link RetryUnwindException} will be
* thrown to unwind the stack to the given scope. This unwinding is
* dependent upon catch blocks being implemented with a corresponding call
* to {@link #honorRetry}.
* <p>
* If the current block is not the target, the retry is deferred by
* marking the target block and then unwinding the stack. Otherwise,
* this method will return naturally. This method is only appropriate
* to be called from within <code>catch</code> blocks since a natural
* return within a <code>catch</code> block will fall through to allow
* the retry loop to engage.
*
* @throws RetryUnwindException
*/
public void triggerRetry()
throws RetryUnwindException
{
TransactionManager.triggerRetry(wa, null);
}
/**
* Force a retry of the current block or the scope named by the given
* label. If a containing scope is the target of the retry, then
* a {@link RetryUnwindException} will be thrown to unwind the stack
* to the given scope. This unwinding is dependent upon catch blocks
* being implemented with a corresponding call to {@link #honorRetry}.
* <p>
* If the current block is not the target, the retry is deferred by
* marking the target block and then unwinding the stack. Otherwise,
* this method will return naturally. This method is only appropriate
* to be called from within <code>catch</code> blocks since a natural
* return within a <code>catch</code> block will fall through to allow
* the retry loop to engage.
*
* @param label
* The name of the scope to retry. Specify <code>null</code> to
* retry the scope of that has a pending rollback OR to retry
* the current scope if there is no pending rollback.
*
* @throws RetryUnwindException
*/
public void triggerRetry(String label)
throws RetryUnwindException
{
TransactionManager.triggerRetry(wa, label);
}
/**
* Dispatch the given exception to all registered stop condition handlers
* to see if any requires that the STOP condition be vetoed at the
* current scope. If any of the registered handlers indicate that the STOP
* condition should not be honored at this level, the exception is
* rethrown, presumably to be honored at a higher scope, or not at all. In
* the latter case, the program running in the current context will abort.
*
* @param exc
* The exception to be checked.
*
* @throws StopConditionException
* if the STOP condition is not honored at this scope.
*/
public void honorStop(StopConditionException exc)
throws StopConditionException
{
TransactionManager.honorStop(wa, exc);
}
/**
* Check if there is a pending retry of the current block and if so then
* enable retry. This is the "last half" of the processing to unwind the
* stack up to a containing scope that is the target of a retry. The
* "first half" of the unwinding is implemented by throwing a
* {@link RetryUnwindException}. This unwinding is dependent upon catch
* blocks being implemented to call this method.
* <p>
* If the current block is not the target for the pending retry, the
* exception is re-thrown.
*
* @param rue
* The exception to honor or ignore.
*
* @throws RetryUnwindException
*/
public void honorRetry(RetryUnwindException rue)
throws RetryUnwindException
{
TransactionManager.honorRetry(wa, rue);
}
/**
* Check if the current error must unwind all the way to the caller of
* the top-level block in this method (if the ignore flag is set). If
* this is the case then the error is re-thrown. Before this occurs, if
* the current block is the top-level block of this method, then the
* ignore flag will be cleared. If the ignore flag is <code>false</code>
* then this method returns silently.
* <p>
* This code also honors any nested error that was generated while another
* error was being processed AND while nested error mode was enabled. Any
* such stored error will be re-thrown here.
*
* @param err
* The exception to honor or ignore.
*
* @throws ErrorConditionException
*/
public void honorError(ErrorConditionException err)
throws ErrorConditionException
{
TransactionManager.honorError(wa, err);
}
/**
* Notifies the transaction manager that an unexpected exception or error
* has occurred and that any transaction must be aborted. This will
* maintain the state of the system properly, including rolling back any
* open transaction. Once the state is properly maintained, the triggering
* <code>Throwable</code> will be re-thrown as a
* <code>RuntimeException</code>.
* <p>
* If the exception is of type <code>ConditionException</code> or
* <code>RetryUnwindException</code>, this method will do nothing since
* these are used for normal flow of control purposes and are used in
* close coordination with the transaction manager's state. They need
* no other special behavior to be safe. This method is designed to
* handle exceptions (like <code>NullPointerException</code>) which are
* not valid in a converted 4GL program.
*
* @param thr
* The unexpected exception or error.
*
* @throws RuntimeException
*/
public void abnormalEnd(Throwable thr)
throws RuntimeException
{
TransactionManager.abnormalEnd(wa, thr, true);
}
/**
* Notifies the transaction manager that an unexpected exception or error
* has occurred and that any transaction must be aborted. This will
* maintain the state of the system properly, including rolling back any
* open transaction. Once the state is properly maintained, the triggering
* <code>Throwable</code> will be re-thrown as a
* <code>RuntimeException</code>.
* <p>
* If the exception is of type <code>ConditionException</code> or
* <code>RetryUnwindException</code>, this method will do nothing since
* these are used for normal flow of control purposes and are used in
* close coordination with the transaction manager's state. They need
* no other special behavior to be safe. This method is designed to
* handle exceptions (like <code>NullPointerException</code>) which are
* not valid in a converted 4GL program.
*
* @param thr
* The unexpected exception or error.
* @param needsNotify
* <code>true</code> if condition notification must be executed
* (only happens if the <code>thr</code> is a condition).
*
* @throws RuntimeException
*/
public void abnormalEnd(Throwable thr, boolean needsNotify)
throws RuntimeException
{
TransactionManager.abnormalEnd(wa, thr, needsNotify);
}
/**
* Reports the transaction level of the current block.
*
* @return The transaction level.
*/
public int currentTransactionLevel()
{
BlockDefinition blk = wa.blocks.peek();
return blk.level;
}
/**
* Reports if the current scope defines the outermost transaction block
* (which is the scope at which final commit processing must occur).
*
* @return <code>true</code> if the current scope defines the top-level
* transaction.
*/
public boolean isFullTransaction()
{
return wa.isFullTransaction();
}
/**
* Reports if there is a current transaction which is active.
*
* @return <code>true</code> if a transaction is active.
*/
public boolean isTransaction()
{
return wa.transLevel != -1;
}
/**
* Reports if there is a current active transaction at the specified block depth.
*
* @param blockDepth
* Zero-based block depth starting from the outermost block at which transaction
* status is checked.
*
* @return <code>true</code> if a transaction is active at the specified block depth.
*/
public boolean isTransactionAt(int blockDepth)
{
int trans = wa.transLevel;
return trans != -1 && trans <= blockDepth;
}
/**
* Reports if the current block is a top-level block (this corresponds
* with a method-level block).
*
* @return <code>true</code> if top-level block, else <code>false</code>.
*/
public boolean isTopLevelBlock()
{
return TransactionManager.isTopLevelBlock(wa);
}
/**
* Reports if the current block is a external block (this corresponds
* with a method-level block that was derived from a Progress external
* procedure). This will be <code>false</code> for blocks derived from
* internal procedures, functions and triggers.
*
* @return <code>true</code> if it is an external block, else
* <code>false</code>.
*/
public boolean isExternalBlock()
{
BlockDefinition blk = wa.blocks.peek();
return blk.external;
}
/**
* Reports if the current block is a global scoped block. For normal contexts:
* <code>true</code> if the current block is the only block on the stack of block scopes; for
* appserver agent contexts: <code>true</code> if the current block is one of two outermost
* blocks ("appserver-agent" and "startup") on the stack of block scopes.
*
* @return See above.
*/
public boolean isGlobalBlock()
{
int size = wa.blocks.size();
return size == 1 || (size == 2 && wa.isRemote());
}
/**
* Check if this session is an appserver session or not.
*
* @return The {@link WorkArea#remote} state.
*/
public boolean isRemote()
{
return wa.isRemote();
}
/**
* Reports if the current block has the ERROR property.
*
* @return <code>true</code> if the current block has the
* <code>PROP_ERROR</code> bit set in its properties bitmap.
*/
public boolean hasErrorProp()
{
return TransactionManager.hasProperty(wa, PROP_ERROR);
}
/**
* Reports if the current block has the ENDKEY property.
*
* @return <code>true</code> if the current block has the
* <code>PROP_ENDKEY</code> bit set in its properties bitmap.
*/
public boolean hasEndkeyProp()
{
return TransactionManager.hasProperty(wa, PROP_ENDKEY);
}
/**
* Reports if the current block has the STOP property.
*
* @return <code>true</code> if the current block has the
* <code>PROP_STOP</code> bit set in its properties bitmap.
*/
public boolean hasStopProp()
{
return TransactionManager.hasProperty(wa, PROP_STOP);
}
/**
* Reports if the current block has the QUIT property.
*
* @return <code>true</code> if the current block has the
* <code>PROP_QUIT</code> bit set in its properties bitmap.
*/
public boolean hasQuitProp()
{
return TransactionManager.hasProperty(wa, PROP_QUIT);
}
/**
* During global finish processing, this reports if the exit was caused
* by a <code>QuitConditionException</code>.
*
* @return <code>true</code> if this exit is due to a quit condition.
*/
public boolean isProcessingQuit()
{
return wa.processingQuit;
}
/**
* During global finish processing, this sets the flag for an exit that
* was caused by a <code>QuitConditionException</code>.
*
* @param quit
* <code>true</code> if this exit is due to a quit condition.
*/
public void setProcessingQuit(boolean quit)
{
wa.processingQuit = quit;
}
/**
* Report if this context should be treated as headless. A headless
* session is one that has no user interaction.
*
* @return <code>true</code> if this session is headless.
*/
public boolean isHeadless()
{
return wa.headless;
}
/**
* Increases the number (by 1) of interactive language statements
* (statements which took user input except for PAUSE) that have executed
* in the current block since the most recent start scope, iterate or
* retry.
*/
public void incrementInteractions()
{
BlockDefinition blk = wa.blocks.peek();
blk.interactions++;
}
/**
* Resets the number of interactive language statements (statements which
* took user input except for PAUSE) that have executed in the current
* block to 0.
*/
public void resetInteractions()
{
BlockDefinition blk = wa.blocks.peek();
blk.interactions = 0;
}
/**
* Gets the number of interactive language statements (statements which
* took user input except for PAUSE) that have executed in the current
* block since the most recent start scope, iterate or retry.
*
* @return The number of interactions in the current block.
*/
public int getInteractions()
{
BlockDefinition blk = wa.blocks.peek();
return blk.interactions;
}
/**
* Get the rollup value for the current block. If <code>true</code>,
* then any contained blocks that exit will cause the current (containing)
* block's interactions counter to be increased by the contained block's
* interactions counter. Otherwise, no aggregation will occur.
*
* @return The current block's rollup value.
*/
public boolean isRollup()
{
BlockDefinition blk = wa.blocks.peek();
return blk.rollup;
}
/**
* Get the rollup value for the enclosing block. If <code>true</code>,
* then the current block's exit will cause the enclosing (containing)
* block's interactions counter to be increased by the current block's
* interactions counter. Otherwise, no aggregation will occur.
*
* @return The current block's rollup value.
*/
public boolean isEnclosingRollup()
{
return TransactionManager.isEnclosingRollup(wa);
}
/**
* Set the new rollup value for the current block. If <code>true</code>,
* then any contained blocks that exit will cause the current (containing)
* block's interactions counter to be increased by the contained block's
* interactions counter. Otherwise, no aggregation will occur.
*
* @param rollup
* The new rollup value.
*/
public void setRollup(boolean rollup)
{
BlockDefinition blk = wa.blocks.peek();
blk.rollup = rollup;
}
/**
* Notify all registered {@link BatchListener} objects that a batch is
* starting.
*/
public void batchStart()
{
TransactionManager.processBatchNotifications(wa, true);
}
/**
* Notify all registered {@link BatchListener} objects that a batch is
* stopping.
*/
public void batchStop()
{
TransactionManager.processBatchNotifications(wa, false);
}
/**
* Tests the interrupted status of the current thread using
* <code>Thread.interrupted</code>, if <code>true</code> then a
* {@link StopConditionException} will be thrown. The interrupted status
* will be <code>true</code> if a call to <code>Thread.interrupt</code>
* was previously made.
* <p>
* The thread's interrupted status is cleared by this method before the
* <code>StopConditionException</code> is thrown.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
* @throws SilentUnwindException
* If the connection ended prematurely end the current thread
* needs to stop.
*/
public void honorStopCondition()
throws StopConditionException,
SilentUnwindException
{
wa.honorStopCondition();
}
/**
* Method called when the processing needs to be terminated. This will set a flag which will
* indicate that the processing should be ended by raising a {@link
* LeaveUnwindException error}, on the next {@link #honorStopCondition()} call.
*/
public void abortProcessing()
{
if (workArea.get(false) == null)
{
// if the context does not exist, do not create it at this time
return;
}
wa.abortProcessing = true;
}
/**
* Enable/disable nested error mode. This mode allows for storing an error
* which is generated during the processing of a different error, so that
* during {@link #honorError} this can be re-thrown. This duplicates some
* obscure behavior seen in Progress.
*
* @param mode
* <code>true</code> to enable, <code>false</code> to disable.
*/
public void setNestedMode(boolean mode)
{
wa.nested = mode;
}
/**
* Reports on the enable/disable state of nested error mode. This mode
* allows for storing an error which is generated during the processing of
* a different error, so that during {@link #honorError} this can be
* re-thrown.
*
* @return <code>true</code> if nested error mode is enabled.
*/
public boolean isNestedMode()
{
return wa.nested;
}
/**
* Returns the current block type.
*
* @return The type of the current block.
*/
public BlockType getBlockType()
{
BlockDefinition blk = wa.blocks.peek();
return blk.type;
}
/**
* Get the top {@link BlockDefinition}.
*
* @return See above.
*/
public BlockDefinition getBlock()
{
return wa.blocks.peek();
}
/**
* Returns the index of the full transaction block, or -1 if none exists.
*
* @return The index of the full transaction block, or -1 if none exists.
*/
public int getFullTransactionBlock()
{
return TransactionManager.getFullTransactionBlock(wa);
}
/**
* Returns the current nesting level.
*
* @return the current nesting level.
*/
public int getNestingLevel()
{
return wa.blocks.size();
}
/**
* Check if this is block is the block associated with the root external program,
* in the 4GL legacy stack.
*
* @return See above.
*/
public boolean isRootNestingLevel()
{
int nestingLevel = getNestingLevel();
return nestingLevel == (wa.isRemote() ? 3 : 2);
}
/**
* Check if this is block is the block associated with the root external program,
* in the 4GL legacy stack using the nearest external block.
*
* @return See above.
*/
public boolean isRootExternal()
{
return TransactionManager.isRootExternal();
}
/**
* Store the given exception for re-throwing during {@link #honorError} if
* nested error mode is enabled. Note that only 1 error can ever be nested
* and the last one set via this method will be the one that is honored.
*
* @param err
* The error to store.
*/
public void saveNestedError(ConditionException err)
{
if (wa.nested)
{
wa.nestedError = err;
}
}
/**
* Notifies this class that an interactive pause has occurred. This may
* affect the processing of infinite loop protection depending on the
* type of block being processed. This state is set for the remainder of
* the current iteration of the innermost block.
*/
public void hadPause()
{
// peek at the current block def on the top of the stack
BlockDefinition blk = wa.blocks.peek();
blk.hadPause = true;
}
/**
* Search the stack of scopes and return the level of the first external
* block found.
*
* @return The stack level of the nearest enclosing external block or
* -1 if there is no block marked as an external block. Level
* number is 1-based starting from the outermost scope.
*/
public int findNearestExternal()
{
return TransactionManager.findNearestExternal(wa);
}
/**
* This method is called before the initialization of a FOR or FOR EACH
* block. It will clear the {@link WorkArea#offEndQueries} set and will
* set the {@link WorkArea#offEndRegistration} flag to ON. While this flag
* is ON, all created queries will be collected in the
* {@link WorkArea#offEndQueries} set.
* <p>
* After the block initialization is finished, the
* {@link #stopOffEndRegistration()} will be called.
*/
public void startOffEndRegistration()
{
wa.offEndRegistration = true;
wa.offEndQueries.push(new HashSet<>());
}
/**
* This method is called after the initialization of a FOR or FOR EACH
* block is finished. It will save all the {@link QueryOffEndListener}s
* associated with the registered {@link WorkArea#offEndQueries queries}.
*/
public void stopOffEndRegistration()
{
TransactionManager.stopOffEndRegistration(wa);
}
/**
* Register the passed query for query off-end events. The passed queries
* are all the queries created in the init method for a FOR or FOR EACH
* block.
* <p>
* These queries will be able to end the loop when the last record is
* reached, by overriding any user-defined END-KEY action. The action
* wich is executed is the default UNDO, LEAVE action for END-KEY.
*
* @param query
* The query which needs to be registered.
*/
public void registerOffEndQuery(P2JQuery query)
{
TransactionManager.registerOffEndQuery(wa, query);
}
/**
* Check if the given listener is registered for query off-end events and
* can override the default END-KEY action (which is UNDO, LEAVE), when
* the current block is FOR or FOR EACH.
*
* @param listener
* A listener to be checked if is registered for off-end events.
*
* @return <code>true</code> if the listener is registered for query
* off-end event.
*/
public boolean isOffEndListener(QueryOffEndListener listener)
{
return TransactionManager.isOffEndListener(wa, listener);
}
/**
* Iterate all registered off-end listeners and call the
* {@link QueryOffEndListener#initialize() initialize} or
* {@link QueryOffEndListener#finish() finish} method, depending on the
* flag.
*
* @param init
* <code>true</code> if the listeners are initialized; else, the
* {@link QueryOffEndListener#finish()} method is called.
*/
public void processOffEndListeners(boolean init)
{
TransactionManager.processOffEndListeners(wa, init);
}
/**
* Get the topmost set of off-end {@link QueryOffEndListener listeners}.
*
* @param clear
* <code>true</code> if the top-most off-end listeners set
* should be cleared.
*/
public Set<QueryOffEndListener> getOffEndListeners(boolean clear)
{
return TransactionManager.getOffEndListeners(wa, clear);
}
/**
* Register the single output parameter assigner for the current block.
* This object will be deregistered by the {@link BlockManager} when it is
* time to process assign-backs from function/procedure output parameters
* to the database record fields with which they are associated.
*
* @param opa
* An output parameter assigner.
*
* @throws IllegalStateException
* if a second output parameter assigner is registered for a block
* before the current one is deregistered. There can be only one.
*/
public void registerOutputParameterAssigner(OutputParameterAssigner opa)
{
TransactionManager.registerOutputParameterAssigner(wa, opa);
}
/**
* Defer handling of an exception thrown by a commit/rollback notification
* callback on a registered {@link Commitable}, so that iteration or pop
* scope processing can be continued without interruption. This ensures
* that all <code>Commitable</code>s registered after the errant one still
* receive their commit/rollback notifications, and that (in the case of
* popping a scope), registered {@link Scopeable}s still receive their
* scope finished notifications.
* <p>
* If no error is already pending, <code>exc</code> is stored, to be thrown
* upon conclusion of processing the current block scope or iteration. If
* an error is already pending, <code>exc</code> is discarded. In either
* case, <code>exc</code> is logged if error-level logging is enabled.
*
* @param action
* A brief description of the action occurring at the time the
* error was encountered, such as "commit" or "master rollback".
* @param thr
* The error caught during processing of a commit or rollback
* notification.
*
* @see WorkArea#handleDeferredError
*/
public void deferError(String action, Throwable thr)
{
TransactionManager.deferError(wa, action, thr, false);
}
/**
* Defer handling of an exception thrown by a commit/rollback notification
* callback on a registered {@link Commitable}, so that iteration or pop
* scope processing can be continued without interruption. This ensures
* that all <code>Commitable</code>s registered after the errant one still
* receive their commit/rollback notifications, and that (in the case of
* popping a scope), registered {@link Scopeable}s still receive their
* scope finished notifications.
* <p>
* If no error is already pending, <code>exc</code> is stored, to be thrown
* upon conclusion of processing the current block scope or iteration. If
* an error is already pending, <code>exc</code> is discarded. In either
* case, <code>exc</code> is logged if error-level logging is enabled.
*
* @param action
* A brief description of the action occurring at the time the
* error was encountered, such as "commit" or "master rollback".
* @param thr
* The error caught during processing of a commit or rollback
* notification.
* @param silent
* If <code>true</code> then exception is not logged.
*
* @see WorkArea#handleDeferredError
*/
public void deferError(String action, Throwable thr, boolean silent)
{
TransactionManager.deferError(wa, action, thr, silent);
}
/**
* Search the stack of scopes and return the level of the first top level
* block found.
*
* @return The stack level of the nearest enclosing top level block or
* -1 if there is no block marked as a top level block.
*/
public int findNearestTopLevel()
{
return TransactionManager.findNearestTopLevel(wa);
}
/**
* Search the stack of scopes and return the type of first top level block found.
*
* @return The nearest enclosing top level block or {@link BlockType#UNKNOWN} if
* there is no block marked as a top level block.
*/
public BlockType getNearestTopLevelType()
{
return TransactionManager.getNearestTopLevelType(wa);
}
/**
* Attempt to initiate a rollback. A rollback is initiated if the transaction level
* of the current scope is a SUB-TRANSACTION or TRANSACTION and if there is no pending
* rollback. If a rollback is already pending a rollback will be initiated when the scope
* is being popped. See {@link #popScope()}.
*/
public void attemptRollback()
{
if (currentTransactionLevel() != NO_TRANSACTION && !wa.isRollbackPending())
{
rollback();
}
}
/**
* Worker that notifies all objects in the commit list that they must
* commit (persist any state).
* <p>
* On the last iteration of a loop or block which ends in success (in
* which the flow of control reaches the natural end of the block), both
* will be called in succession. However, other forms of exit which are
* still considered successful (i.e. return and break which are not the
* result of condition processing) will only execute <code>popScope</code>
* and <code>iterate</code> will be bypassed.
* <p>
* It is important to note that the list of commitables in blk is
* iterated in reverse order from that in which they are registered with
* the <code>TransactionManager</code>. This is done to ensure that
* database transactions (which are registered before their corresponding
* buffers) are always committed after all buffers are committed.
*
* @throws ErrorConditionException
* If validation fails.
*/
void processValidate()
throws ErrorConditionException
{
wa.processValidate();
}
/**
* Get the BlockDefinition instance of the nearest block of one of the
* following types:
* <ul>
* <li>{@link BlockType#EXTERNAL_PROC external procedure}</li>
* <li>{@link BlockType#INTERNAL_PROC internal procedure}</li>
* <li>{@link BlockType#FUNCTION user-defined function}</li>
* <li>{@link BlockType#TRIGGER trigger}</li>
* <li>{@link BlockType#DATABASETRIGGER trigger}</li>
* <li>{@link BlockType#EDITING editing}</li>
* </ul>
*
* @return See above.
*/
BlockDefinition nearestExecutingBlock()
{
return TransactionManager.nearestExecutingBlock(wa);
}
/**
* Get the executing block at the specified level.
*
* @param levels
* Zero-based depth of the target block, starting from the innermost block.
*
* @return The found block, or <code>null</code> if the index is outside of the block depth.
*/
BlockDefinition getBlock(int levels)
{
return TransactionManager.getBlock(wa, levels);
}
/**
* Deregister the output parameter assigner for the current block, if any.
* It is expected that only the {@link BlockManager} will invoke this
* method.
*
* @return Output parameter assigner for this block, or <code>null</code>
* if none was registered.
*/
OutputParameterAssigner deregisterOutputParameterAssigner()
{
return TransactionManager.deregisterOutputParameterAssigner(wa);
}
/**
* Adds the given object into the list of objects which require undo
* notifications for the next block that is opened.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block. In particular, it is necessary for
* objects that are constructed during a class' constructor/initializer
* (before the external procedure block has opened). Such objects cannot
* simply register since their construction is not actually enclosed by the
* block.
* <p>
* When the deferred registration occurs, it will NOT be global and will
* NOT be added to registered Commitables.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
* <p>
* The given object will only be added if it is not already in the list.
*
* @param target
* The object references to be registered. This cannot be
* <code>null</code>.
*/
void registerNext(LazyUndoable... target)
{
TransactionManager.registerAt(wa, ScopeLevel.NEXT, target);
}
/**
* Adds the given object into the list of objects which require undo
* notifications for the next block that is opened.
* <p>
* This form of registration is useful for objects whose natural lifetime
* is scoped to the top-level block. In particular, it is necessary for
* objects that are constructed during a class' constructor/initializer
* (before the external procedure block has opened). Such objects cannot
* simply register since their construction is not actually enclosed by the
* block.
* <p>
* When the deferred registration occurs, it will NOT be global and will
* NOT be added to registered Commitables.
* <p>
* This notification will occur whether or not a transaction is active.
* <p>
* These registrations naturally are removed when the associated scope
* is removed.
* <p>
* The given object will only be added if it is not already in the list.
*
* @param scope
* The scope where to register the undoable targets.
* @param target
* The object references to be registered. This cannot be
* <code>null</code>.
*/
void registerAt(ScopeLevel scope, LazyUndoable... target)
{
TransactionManager.registerAt(wa, scope, target);
}
/**
* Register the specified targets as undoables. This will mark the object as undoable and save
* the current tx level, regardless if a tx is active or not.
*
* @param undoTargets
* List of object references to be registered. This cannot be
* <code>null</code>.
*/
public void registerCurrent(LazyUndoable... undoTargets)
{
TransactionManager.registerCurrent(wa, undoTargets);
}
/**
* Adds the object into a scoped dictionary of objects requiring UNDO support. This registry
* is used to backup the objects at the beginning of transactions or sub-transactions.
* <p>
* These registrations naturally are removed when the associated scope is removed. However,
* the global scope is long-lived. Special care is necessary to prevent the same instance of
* global shared variables being added multiple times.
*
* @param transLevel
* The transaction level to set for this undoable.
* @param persistent
* Flag indicating if the undoable is defined in a persistent procedure
* @param When not-null, persistent must be true and the referent represents the instance where to
* look for undoables. Otherwise, THIS-PROCEDURE will be used.
* @param undoTargets
* The object references to be rolled back on failure. This
* cannot be <code>null</code>.
*/
void registerAt(int transLevel, boolean persistent, Object referent, LazyUndoable... undoTargets)
{
TransactionManager.registerAt(wa, transLevel, persistent, referent, undoTargets);
}
/**
* Adds the object into a scoped dictionary of objects requiring UNDO support. This registry
* is used to backup the objects at the beginning of transactions or sub-transactions.
* <p>
* These registrations naturally are removed when the associated scope is removed. However,
* the global scope is long-lived. Special care is necessary to prevent the same instance of
* global shared variables being added multiple times.
*
* @param transLevel
* The transaction level to set for this undoable.
* @param persistent
* Flag indicating if the undoable is defined in a persistent procedure
* @param undoTargets
* The object references to be rolled back on failure. This
* cannot be <code>null</code>.
*/
void registerAt(int transLevel, boolean persistent, LazyUndoable... undoTargets)
{
TransactionManager.registerAt(wa, transLevel, persistent, undoTargets);
}
/**
* Enable tracking of all the undaobles defined in the next instantiating external program.
*/
void trackUndoables()
{
wa.trackUndoables = true;
wa.trackedUndoables = new IdentityHashMap<>();
}
/**
* Untrack (by removing from all the tx blocks) all the undoables defined and registered for
* the specified external program.
*
* @param referent
* The external program instance.
*/
void untrackUndoables(Object referent)
{
TransactionManager.untrackUndoables(wa, referent);
}
/**
* Rollback the transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*
* @param chained
* Flag indicating if a new transaction will be started.
*/
void rollbackTx(boolean chained)
{
TransactionManager.rollbackTx(wa, chained);
}
/**
* Commit the transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*
* @param chained
* Flag indicating if a new transaction will be started.
*/
void commitTx(boolean chained)
{
TransactionManager.commitTx(wa, chained);
}
/**
* Begin an explicit transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
* <p>
* This explicitly marks this tx as being open by the
* {@link TransactionResource#getTxInitProcedure() TRANS-INIT-PROCEDURE}.
*/
void beginTx()
{
TransactionManager.beginTx(wa, null);
}
/**
* End an explicit transaction, at the {@link #APPSERVER_ROOT_BLOCK_INDEX root FWD block}.
*/
void endTx()
{
TransactionManager.endTx(wa, null);
}
/**
* Detect if the top-most block is a database trigger
* @return
* True if the top-most block is a database trigger, false otherwise
*/
public boolean isTopLevelDatabaseTrigger()
{
return TransactionManager.isTopLevelDatabaseTrigger(wa);
}
/**
* Used to make a weak block stack snapshot. The stack is a copy while the content
* should still reference the original definitions.
*
* @return A new stack containing the same block definitions.
*/
public Stack<BlockDefinition> copyStack()
{
Stack<BlockDefinition> copy = new Stack<>();
copy.addAll(wa.blocks);
return copy;
}
}
/**
* Helper class to expose context-local state for undoable support.
*/
static class UndoableHelper
{
/** The {@link WorkArea} instance. */
private WorkArea wa;
/**
* Create a new instance and associate the given WorkArea instance.
*
* @param wa
* The {@link WorkArea} instance.
*/
public UndoableHelper(WorkArea wa)
{
this.wa = wa;
}
/**
* Obtain the context-local error helper.
*
* @return The error helper.
*/
public ErrorManager.ErrorHelper getErrorHelper()
{
return wa.em;
}
/**
* Register the undoable with all transaction blocks (currently in the stack) started after
* the given transaction level.
*
* @param src
* The undoable instance.
*
* @return The current transaction level, as in {@link WorkArea#txNestingLevel}.
*/
public int registerUndoable(LazyUndoable src)
{
// the transaction level at which the given Undoable was last registered.
int transLevel = src.getTransLevel();
if (wa.txNestingLevel <= transLevel)
{
// nothing to do, no new tx blocks on the stack
return wa.txNestingLevel;
}
boolean global = src.isGlobal();
LazyUndoable snapshot = null;
BlockDefinition blk = null;
int idx = wa.blocks.size() - 1;
while (idx >= 0)
{
blk = wa.blocks.get(idx);
if (blk.level == NO_TRANSACTION)
{
idx = idx - 1;
continue;
}
// global vars register all the way up the stack, until the full tx block is found
if (!global && blk.txNestingLevel <= transLevel)
{
break;
}
if (blk.undoables == null)
{
blk.undoables = new ArrayList<>();
}
if (snapshot == null)
{
if (global && src instanceof BaseDataType)
{
// TODO: this is an attempt to fix some quirks with NEW GLOBAL SHARED variables
// which are undone in a outer scope, greater than the defining one: in this
// case, the var seems to undo to unknown, but the solution is not yet complete.
// only the first global var gets undone, the other get 'deleted'.
snapshot = ((BaseDataType) src).instantiateUnknown();
}
else
{
snapshot = (LazyUndoable) src.deepCopy();
}
}
blk.undoables.add(new UndoablePair(src, snapshot));
idx = idx - 1;
if (blk.full)
{
break;
}
}
return wa.txNestingLevel;
}
/**
* Check if undoable can be processed.
*
* @param currentBlock
* {@code true} to check only for the current block;
* {@code false} to check only if a transaction has been started.
*
* @return {@code true} if undoable can be processed.
*/
public boolean processUndoables(boolean currentBlock)
{
return wa.processUndoables(currentBlock);
}
}
/**
* Stores global data relating to the state of the current context's
* transactions. Includes convenience methods for notifying listeners.
*/
private static class WorkArea
{
/** Sprintf format specification for string representation. */
public static final String spec = "<depth = %d; trans_level = %d; " +
"trans_label = %s; " +
"rollback_scope = %d; " +
"rollback_label = %s; " +
"rollback_pending = %b; in_quit = " +
"%b; retry_scope = %d; " +
"retry_label = %s; " +
"ignore_err = %b>";
/** Helper to use the ProcedureManager without any context local lookups. */
public ProcedureManager.ProcedureHelper pm;
/** Helper to use the ErrorManager without any context local lookups. */
public ErrorManager.ErrorHelper em;
/** The buffer manager instance, active only on server-side. */
public BufferManager bm;
/** The logical terminal instance, active only on server-side. */
public LogicalTerminal lt;
/** The set of pending scopeables to be registered for the next block. */
private Scopeable[] pendingScopeables = null;
/** StopAfterTimer of the current conversation block. */
private StopAfterTimer stopAfterTimer = null;
/**
* Map of Commitables to get a commit notification after all other commits occur in a full transaction;
* keys are the listeners, values are flags indicating whether listeners are auto-removed.
*/
private Map<Object, Boolean> masterCommit = null;
/**
* Map of Finalizables to get a finish notification after all other finish notifications occur in a full
* transaction; keys are the listeners, values are flags indicating whether listeners are auto-removed.
*/
private Map<Object, Boolean> masterFinish = null;
/** Map of BatchListeners to get batch start and stop notifications. */
private final List<BatchListener> batchListeners = new LinkedList<>();
/** Stack level of the full transaction if one is currently active. */
// having a package-private field avoids a synthetic getter method emitted by javac
int transLevel = -1;
/** The number of blocks with full or sub-transaction level, in the current stack.*/
private int txNestingLevel = -1;
/** Defines the scope at which rollback will occur. */
private int rollbackScope = -1;
/** Flag to denote if a rollback is pending. */
private boolean rollbackPending = false;
/** Cause (if available) of pending rollback. */
private Throwable rollbackPendingCause = null;
/** Flag to denote if the exit is due to a quit condition. */
private boolean processingQuit = false;
/** Defines the scope at which a pending retry will occur. */
private int retryScope = -1;
/**
* Defines if an error condition should be ignored until reaching the
* caller of a top-level block.
*/
private boolean ignoreError = false;
/** Throwable caught during commit/rollback processing but deferred. */
private Throwable deferredError = null;
/** Enables/disables nested error support at runtime. */
private boolean nested = false;
/** Exception caught during processing another error. */
private ConditionException nestedError = null;
/** Defines a single global scope which is not kept in the stack. */
private BlockDefinition globalBlock = null;
/**
* Tracks block definitions with the top of the stack as the innermost
* scope.
*/
// having a package-private field avoids a synthetic getter method emitted by javac
Stack<BlockDefinition> blocks = new Stack<>();
/** <code>Finalizable</code>s which need deferred registration. */
private ArrayList<Finalizable> deferredExtFinalizables = null;
/** <code>Undoable</code>s which need deferred registration. */
private IdentityHashMap<LazyUndoable, Object> deferredUndoables = null;
/** Set of undoable dynamic extent vars, to be registered with the invoked procedure. */
private Set<BaseDataType[]> deferredDynExt = null;
/** Map of undoables defined in persistent procedures. */
private Map<Object, IdentityHashMap<LazyUndoable, Object>> procUndoables = new IdentityHashMap<>();
/**
* Flag indicating if undoables need to be tracked at ext program instantiation, thus to
* be registered with a persistent procedure. It will be activated only when the
* instantiated external program is being ran persistent.
*/
private boolean trackUndoables = false;
/**
* The map of undoables which are defined in a new instantiated external program, ran
* persistent.
*/
private IdentityHashMap<LazyUndoable, Object> trackedUndoables = null;
/** Collection or map currently being iterated. */
private Object nowIterating = null;
/** Current operation. */
private int op = OP_NONE;
/** Registered STOP condition handlers */
private List<StopConditionVetoHandler> stopHandlers = new LinkedList<>();
/**
* A stack of registered query off-end listeners, which interrupt the
* FOR or FOR EACH block when a {@link QueryOffEndException} is
* caught (the user defined END-KEY action is ignored and will perform
* a UNDO, LEAVE).
*/
private ArrayDeque<Set<QueryOffEndListener>> offEndListeners = new ArrayDeque<>(10);
/**
* Registered <code>Resettable</code>s which should be reset at
* specific points of block processing.
*/
private ArrayList<Resettable> resettables = new ArrayList<>(1);
/**
* A stack of sets of queries registered during FOR or FOR EACH block initialization. Only
* {@link QueryOffEndException} triggered by the queries from a given set can end a FOR or FOR EACH
* block, even if the END-KEY action is overridden. The sets are organized in the stack because
* in the {@code init()} section of a block not only off-end query registration can happen, but also
* buffer flush and call of database triggers which may have their own FOR or FOR EACH blocks.
*/
private ArrayDeque<Set<P2JQuery>> offEndQueries = new ArrayDeque<>();
/**
* Flag indicating that the block executes its {@link Block#init()}
* method. Only variants of FOR or FOR EACH blocks will set the flag to
* <code>true</code>. During block initialization, all created queries
* will be collected in the {@link #offEndQueries} set.
*/
private boolean offEndRegistration = false;
/**
* Value of {@code offEndRegistration} flag saved for the period of off-end query registration pause.
*/
private boolean savedOffEndRegistration = false;
/**
* Flag indicating the processing should be terminated.
*/
private volatile boolean abortProcessing = false;
/** Flag that disables calls to the client side. */
private boolean headless = false;
/**
* The cached {@link ProcedureManager}'s scopeable instance, to avoid resolving
* context-local data.
*/
private Scopeable procScopeable;
/** Cached value of the {@link AppServerManager#isRemote()} state.*/
private Boolean remote = null;
/** Flag indicating if this is a user-interactive session. */
private Boolean interactive = null;
/** Keep stop-after exception in order to re-throw it to the timeout block */
private LegacyStopException stopAfterException = null;
/**
* Worker that notifies all objects in the commit list that they must
* commit (persist any state).
* <p>
* On the last iteration of a loop or block which ends in success (in
* which the flow of control reaches the natural end of the block), both
* will be called in succession. However, other forms of exit which are
* still considered successful (i.e. return and break which are not the
* result of condition processing) will only execute <code>popScope</code>
* and <code>iterate</code> will be bypassed.
* <p>
* It is important to note that the list of commitables in blk is
* iterated in reverse order from that in which they are registered with
* the <code>TransactionManager</code>. This is done to ensure that
* database transactions (which are registered before their corresponding
* buffers) are always committed after all buffers are committed.
*
* @throws ErrorConditionException
* If validation fails.
*/
private void processValidate()
throws ErrorConditionException
{
if (FwdServerJMX.JMX_DEBUG)
{
VALIDATE.timer(this::processValidateImpl);
}
else
{
processValidateImpl();
}
}
/**
* Worker that notifies all objects in the commit list that they must
* commit (persist any state).
* <p>
* On the last iteration of a loop or block which ends in success (in
* which the flow of control reaches the natural end of the block), both
* will be called in succession. However, other forms of exit which are
* still considered successful (i.e. return and break which are not the
* result of condition processing) will only execute <code>popScope</code>
* and <code>iterate</code> will be bypassed.
* <p>
* It is important to note that the list of commitables in blk is
* iterated in reverse order from that in which they are registered with
* the <code>TransactionManager</code>. This is done to ensure that
* database transactions (which are registered before their corresponding
* buffers) are always committed after all buffers are committed.
*
* @throws ErrorConditionException
* If validation fails.
*/
private void processValidateImpl()
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "processValidate", null);
}
BlockDefinition blk = blocks.peek();
if (blk.commitables == null)
{
return;
}
try
{
nowIterating = blk.commitables;
boolean aggressiveFlush = blk.forFlushing || blk.external;
ArrayList<Commitable> commitables = blk.commitables;
for (int i = 0; i < commitables.size(); i++)
{
Commitable target = commitables.get(i);
target.validate(blk.full, aggressiveFlush);
}
}
finally
{
nowIterating = null;
}
}
/**
* Check if the current session is interactive (i.e. not appserver agent or batch process).
*
* @return See above.
*/
private boolean isInteractive()
{
if (interactive == null)
{
interactive = !(isRemote() || LogicalTerminal.isInBatchMode());
}
return interactive;
}
/**
* Check if this session is an appserver session or not.
*
* @return The {@link #remote} state.
*/
private boolean isRemote()
{
if (remote == null)
{
remote = AppServerManager.isRemote();
}
return remote.booleanValue();
}
/**
* Reports if the current scope defines the outermost transaction block
* (which is the scope at which final commit processing must occur).
*
* @return <code>true</code> if the current scope defines the top-level
* transaction.
*/
private boolean isFullTransaction()
{
BlockDefinition blk = blocks.peek();
return blk.full;
}
/**
* Reports if there is a current transaction which is active.
*
* @return <code>true</code> if a transaction is active.
*/
private boolean isTransaction()
{
return transLevel != -1;
}
/**
* Reports if there is a rollback operation pending.
*
* @return <code>true</code> if a rollback has been requested but not
* yet processed.
*/
private boolean isRollbackPending()
{
return rollbackPending;
}
/**
* Reports if there is a current transaction which is active.
*
* @return <code>true</code> if a transaction is active.
*/
private boolean isTransactionBlock()
{
return (transLevel != -1) && (transLevel == (blocks.size() - 1));
}
/**
* Starts a new top-level transaction (requires that there is no current
* transaction which is active).
*
* @param level
* Stack level of the scope that is to be specified as the top
* level scope.
*/
private void setTransactionLevel(int level)
{
if (transLevel != -1 && level != -1)
{
throw new IllegalArgumentException("Transaction is already active!");
}
transLevel = level;
// 'close' all active transactions
if (!isTransaction())
{
synchronized (transactionData)
{
SecurityManager sm = SecurityManager.getInstance();
if (sm != null)
{
Integer userId = sm.sessionSm.getSessionId();
if (userId != null)
{
for (Map.Entry<String, DbTransactions> entry : transactionData.entrySet())
{
DbTransactions dbData = entry.getValue();
// do not remove the [entry] from [transactionData] even if there are
// no more transactions in the database because it also keeps the
// transactions counter that will be used for the upcoming transactions
dbData.transactions.remove(userId);
// mark the in-memory as dirty in order to remove the record with
// next flush operation
dbData.isDirty = true;
}
}
}
}
}
}
/**
* Tests the interrupted status of the current thread using
* <code>Thread.interrupted</code>, if <code>true</code> then a
* {@link StopConditionException} will be thrown. The interrupted status
* will be <code>true</code> if a call to <code>Thread.interrupt</code>
* was previously made.
* <p>
* The thread's interrupted status is cleared by this method before the
* <code>StopConditionException</code> is thrown.
*
* @throws StopConditionException
* If the current thread's was previously interrupted.
* @throws SilentUnwindException
* If the connection ended prematurely end the current thread
* needs to stop.
*/
private void honorStopCondition()
throws StopConditionException,
SilentUnwindException
{
if (abortProcessing)
{
throw new SilentUnwindException();
}
if (Thread.interrupted())
{
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE,
"honorStopCondition",
"STOP (honoring deferred thread interruption)!");
}
String errmsg = "Thread was asynchronously interrupted.";
StopConditionException exe = null;
if (stopAfterTimer != null && stopAfterTimer.isStopAfterConditionRaised())
{
exe = new StopAfterConditionException(errmsg);
}
else
{
exe = new StopConditionException(errmsg);
}
ErrorManager.handleStopException(exe);
}
}
/**
* If the LegacyStopException should be propagated up the stack
* to the block that raised the STOP condition after a stop-after
* timeout.
*
* @throws blk
* BlockDefinition of the current block.
*/
private void honorStopAfterCondition(BlockDefinition blk)
throws LegacyStopException
{
LegacyStopException lex = stopAfterException;
if (lex == null)
{
return;
}
ObjectVar<? extends Stop> error = lex.getError();
if (stopAfterTimer != null && !stopAfterTimer.isStoppedBlock(blk))
{
throw lex;
}
else if (stopAfterTimer != null && stopAfterTimer.isStopAfterConditionRaised())
{
if (stopAfterTimer.isStoppedBlock(blk))
{
stopAfterTimer.cleanUp();
}
// we need a decrement here, as the instance is also 'hooked' by LegacyErrorException
error.decrement();
// release this reference
error.set(null);
stopAfterException = null;
}
}
/**
* Returns the level specifying the rollback scope that is pending.
*
* @return The level of the scope to be rolled back or -1 if the
* closest containing transaction or sub-transaction should
* be rolled back.
*/
private int getRollbackScope()
{
return rollbackScope;
}
/**
* Handle a pending, deferred error, represented by an exception thrown
* by a commit or rollback notification callback on a registered {@link
* Commitable}, and caught within {@link #processCommit} or {@link
* #processRollback}, respectively. If there is currently no deferred
* error, this method returns immediately. Otherwise, a debug message
* is logged, the deferred error is reset to <code>null</code>, and the
* stored exception is thrown. It is intended that this method is called
* at the end of iterate or pop scope processing, after all registered
* <code>Commitable</code>s and <code>Scopeable</code>s have been notified
* of the state change.
*
* @see #deferError
*/
private void handleDeferredError()
throws RuntimeException
{
if (deferredError == null)
{
// nothing to do
return;
}
if (LOG.isLoggable(Level.SEVERE))
{
String msg = "Throwing deferred error (" + deferredError.getMessage() + ")";
log(Level.SEVERE, "handleDeferredError", msg);
}
// reset the error condition
Throwable thr = deferredError;
deferredError = null;
// condition exceptions get thrown directly since they are "safe"
if (!(thr instanceof ConditionException))
{
// anything else is wrapped in an unchecked exception and becomes an abnormal end
thr = new RuntimeException("Deferred error processing.", thr);
}
// honor the error
throw (RuntimeException) thr;
}
/**
* Report scope depth.
*
* @return Number of blocks currently on stack.
*/
private int depth()
{
return blocks.size();
}
/**
* Notify all master Finalizables of a retry event. Listeners are
* notified in the order in which they were registered.
*/
private void notifyMasterRetry()
{
if (masterFinish == null)
{
return;
}
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "notifyMasterRetry", null);
}
try
{
nowIterating = masterFinish;
op = OP_RETRY;
for (Object it : masterFinish.keySet())
{
// Process notification, deferring any error until we are in a
// safe state to handle it.
Finalizable target = (Finalizable) it;
try
{
target.retry();
}
catch (Throwable thr)
{
deferError("master retry", thr);
}
}
}
finally
{
nowIterating = null;
op = OP_NONE;
}
}
/**
* Notify all master Finalizables of an iteration or finished event.
* <p>
* After each notification, the corresponding {@code Finalizable} is removed, if it was registered as
* auto-removable. Listeners are notified in the order in which they were registered.
*
* @param iterate
* {@code true} to notify of an iterate event; {@code false} to notify of a finished event.
*/
private void notifyMasterFinish(boolean iterate)
{
if (masterFinish == null)
{
return;
}
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "notifyMasterFinish", "iterate = " + iterate);
}
try
{
nowIterating = masterFinish;
op = iterate ? OP_ITERATE : OP_FINISHED;
for (Object it : masterFinish.keySet())
{
// process notification, deferring any error until we are in a safe state to handle it
Finalizable target = (Finalizable) it;
if (iterate)
{
try
{
target.iterate();
}
catch (Throwable thr)
{
deferError("master iterate", thr);
}
}
else
{
try
{
target.finished();
}
catch (Throwable thr)
{
deferError("master finish", thr);
}
}
}
}
finally
{
nowIterating = null;
op = OP_NONE;
}
}
/**
* Notify all master Commitables of a rollback or commit event.
* After each notification, the corresponding Commitable is removed,
* if it was registered as auto-removable and <code>autoRemove</code>
* is <code>true</code>. Listeners are notified in the order in which
* they were registered.
*
* @param rollback
* <code>true</code> to notify of a rollback event;
* <code>false</code> to notify of a commit event.
*/
private void notifyMasterCommit(boolean rollback)
{
if (masterCommit == null)
{
return;
}
if (LOG.isLoggable(Level.FINE))
{
log(Level.FINE, "notifyMasterCommit", "rollback = " + rollback);
}
try
{
nowIterating = masterCommit;
for (Object it : masterCommit.keySet())
{
// Process notification, deferring any error until we are in a safe state to handle it.
Commitable target = (Commitable) it;
if (rollback)
{
try
{
target.rollback(true);
}
catch (Throwable thr)
{
deferError("master rollback", thr);
}
}
else
{
try
{
target.commit(true);
}
catch (Throwable thr)
{
deferError("master commit", thr);
}
}
}
}
finally
{
nowIterating = null;
}
}
/**
* Iterate through registered, master Finalizables and deregister those marked for auto-remove.
*/
private void autoRemoveMasterFinish()
{
autoRemoveMasterFinish(false);
}
/**
* Iterate through registered, master Finalizables and deregister those marked for auto-remove.
* Before deregistering, it will also call {@link Finalizable#deleted()} based on the parameter
* value given.
*
* @param delete
* If the deleted() method of the Finalizable should be called.
*/
private void autoRemoveMasterFinish(boolean delete)
{
if (masterFinish != null && canRemoveFromMap(masterFinish, "Finalizables"))
{
autoRemoveAndDelete(masterFinish, delete);
if (masterFinish.isEmpty())
{
masterFinish = null;
}
}
}
/** Iterate through registered, master Commitables and deregister those marked for auto-remove. */
private void autoRemoveMasterCommit()
{
if (masterCommit != null && canRemoveFromMap(masterCommit, "Commitables"))
{
autoRemoveAndDelete(masterCommit, false);
if (masterCommit.isEmpty())
{
masterCommit = null;
}
}
}
/**
* Test whether the {@code map} is NOT being iterated so elements stored there can be removed.
*
* @param map
* The map to be tested.
* @param name
* The name/type of the elements contained in the map. Only for debugging.
*
* @return {@code true} if the map can be altered by removing elements or {@code false} when a remove
* operation will cause a {@code ConcurrentModificationException}.
*/
private boolean canRemoveFromMap(Map<Object, Boolean> map, String name)
{
if (nowIterating == map)
{
// prevent ConcurrentModificationException
if (LOG.isLoggable(Level.WARNING))
{
log(Level.WARNING,
"autoRemoveMaster" + name,
"Illegal attempt to auto-remove master " + name,
new Throwable());
}
return false;
}
return true;
}
/**
* Iterate through the given map entries and remove all those whose values are {@code true}.
* The key is a {@code Commitable} or {@code Finalizable} target, the value is its registered
* auto-remove setting. For a {@code Finalizable} target, it also calls the
* {@link Finalizable#deleted()} method.
*
* @param map
* Map of registered targets and their auto-remove settings.
* @param delete
* Should be {@code true} when the map contains Finalizable targets and in this case
* the deleted() method of the Finalizable will be called.
*/
private void autoRemoveAndDelete(Map<Object, Boolean> map, boolean delete)
{
Iterator<Map.Entry<Object, Boolean>> iter = map.entrySet().iterator();
while (iter.hasNext())
{
Map.Entry<Object, Boolean> current = iter.next();
if (current.getValue())
{
if (delete)
{
Object key = current.getKey();
if (key instanceof Finalizable)
{
((Finalizable) key).deleted();
}
}
iter.remove();
}
}
}
/**
* Render this work area's data as a human readable string.
*
* @return The human readable form of the global data.
*/
public String toString()
{
BlockDefinition tblk = null;
BlockDefinition rollbackBlk = null;
BlockDefinition retryBlk = null;
int size = blocks.size();
if (transLevel != -1 && transLevel < size)
{
tblk = blocks.elementAt(transLevel);
}
if (rollbackScope != -1 && rollbackScope < size)
{
rollbackBlk = blocks.elementAt(rollbackScope);
}
if (retryScope != -1 && retryScope < size)
{
retryBlk = blocks.elementAt(retryScope);
}
Object[] parms = new Object[]
{
depth(),
transLevel,
(tblk == null) ? null : tblk.label,
rollbackScope,
(rollbackBlk == null) ? null : rollbackBlk.label,
rollbackPending,
processingQuit,
retryScope,
(retryBlk == null) ? null : retryBlk.label,
ignoreError
};
return String.format(spec, parms);
}
/**
* Check if undoable can be processed.
*
* @param currentBlock
* {@code true} to check only for the current block;
* {@code false} to check only if a transaction has been started.
*
* @return {@code true} if undoable can be processed.
*/
private boolean processUndoables(boolean currentBlock)
{
if (!currentBlock)
{
return this.transLevel != -1;
}
return processUndoables(this.blocks.peek());
}
/**
* Check if undoable can be processed by the current block.
*
* @return {@code true} if a transaction has been started and the current block's
* transaction level is not {@link TransactionManager#NO_TRANSACTION}.
*/
private boolean processUndoables()
{
return processUndoables(this.blocks.peek());
}
/**
* Check if undoable can be processed by the current block.
*
* @param blk
* The target block.
*
* @return {@code true} if a transaction has been started and the current block's
* transaction level is not {@link TransactionManager#NO_TRANSACTION}.
*/
private boolean processUndoables(BlockDefinition blk)
{
return this.transLevel != -1 && blk.level != NO_TRANSACTION;
}
}
/**
* Simple container that stores and returns a context-local instance of
* the global work area.
*/
private static class ContextContainer
extends ContextLocal<WorkArea>
{
/**
* Get the weight of this context-local var.
*
* @return Always {@link WeightFactor#LEVEL_4}.
*/
@Override
public WeightFactor getWeight()
{
return WeightFactor.LEVEL_4;
};
/**
* Obtains the context-local instance of the contained global work
* area. The first time this method is invoked in a context, a new
* <code>WorkArea</code> instance is lazily created.
*
* @return The work area associated with this context.
*/
public WorkArea obtain()
{
WorkArea wa = get();
if (wa == null)
{
wa = new WorkArea();
wa.globalBlock = new BlockDefinition("globalScope",
NO_TRANSACTION,
PROP_NONE,
false,
false,
false,
false,
false,
false,
false,
UNKNOWN);
// detect if headless mode is needed
wa.headless = _isHeadless();
// the following call MUST occur before creating Scopeables
// below, in case a Scopeable initializer calls back into the
// TransactionManager in such a way as to call this class'
// obtain() method (prevents infinite loop and stack overflow)
set(wa);
wa.pm = ProcedureManager.getProcedureHelper();
wa.em = ErrorManager.getErrorHelper();
if (SessionManager.get() != null && !SessionManager.get().isLeaf())
{
// only on server-side
wa.bm = BufferManager.get();
wa.lt = (wa.headless ? null : LogicalTerminal.getInstance());
}
wa.em.setHeadless(wa.headless);
wa.procScopeable = ProcedureManager.getScopeable();
}
return wa;
}
/**
* Return <code>null</code> rather than providing a true initial value;
* initial value is instead lazily created in {@link #obtain}.
*
* @return <code>null</code>.
*/
protected final WorkArea initialValue()
{
return null;
}
}
/**
* The transaction information for a database. The object holds the transaction counter and the
* set of transactions mapped by the connection id of the users that created them.
*/
private static class DbTransactions
{
/** The id of last transaction generated for this database. */
private int lastTransactionId = 0;
/**
* Flags whether a transaction has started or ended since the last flush to persistence.
* This is only used to quickly decide whether there are transactions to be flushed.
*/
private boolean isDirty = false;
/** The set of transaction ids of this database mapped by the users that opened them. */
private Map<Integer, Integer> transactions = new HashMap<>();
}
}