# ProcedureManager.java

    /*
    ** Module   : ProcedureManager.java
    ** Abstract : Implements access to procedure system handles and procedure related attributes and methods.
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------------Description---------------------------------------
    ** 001 CA  20130112 Created initial version.
    ** 002 CA  20130130 Added stubbed methods to implement the PUBLISH, SUBSCRIBE and UNSUBSCRIBE
    **                  language statements.
    ** 003 CA  20130131 Fixed problem determining THIS-PROCEDURE when the trigger invoked is defined
    **                  as ON ... PERSISTENT RUN.
    ** 004 CA  20130202 Update from character to TextOps.
    ** 005 CA  20130529 Some refactoring related to procedure deletion and added appserver support.
    ** 006 CA  20130918 Added runtime support for widget-pools: the pools need scope notifications,
    **                  and this must be done by the ProcedureManager, to ensure proper access to the
    **                  THIS-PROCEDURE handle.
    ** 007 CA  20130925 Added support for named events.
    ** 008 CA  20131013 Implemented implicit deletion of static resources and delayed processing of
    **                  finalizable/scopeable instances. Added support for the INSTANTIATING-PROCEDURE
    **                  attribute.
    ** 009 CA  20131208 Emit SEARCH-TARGET and SEARCH-SELF literals as integer.  Fixed runtime for
    **                  RUN SUPER and SUPER() calls, in search-target and search-self modes.
    ** 010 HC  20131222 Implemented ADM-DATA and UNIQUE-ID attribute.
    **                  INSTANTIATING-PROCEDURE is now managed by ProcedureManager.
    ** 011 EVK 20131223 Made method isProcedure public.
    ** 012 CA  20140109 WorkArea.scopeFinished has no target-procedure to pop, when terminating an
    **                  appserver request.
    ** 013 VIG 20140109 Implemented CURRENT-WINDOW attribute.
    ** 014 CA  20140218 Implemented PROGRAM-NAME. Fixed some SOURCE- and TARGET-PROCEDURE cases (the
    **                  RUN PERSISTENT or RUN ext-prog cases).  Also, the external program name used
    **                  at the RUN statement must be used in error messages; the absolute name must be
    **                  used each time SourceNameMapper APIs are invoked.  Now we allow java-style
    **                  calls for both internal and external procedures.
    ** 015 CA  20140221 Fixed SUPER-PROCEDURES - emit correct handle ID.
    ** 016 CA  20140223 Fixed deletion of persistent procedures via DELETE OBJECT <system-proc-handle>.
    ** 017 CA  20140301 Procedure's scope is notified first for start and last for finish.
    ** 018 HC  20140424 Fixed resolveClosestMethod failing to resolve extent functions.
    ** 019 CA  20140502 Fixed super-procedure chaining in SUPER and RUN SUPER calls.
    ** 020 SVL 20140414 Modified INSTANTIATING-PROCEDURE assignment logic in order to support agent
    **                  contexts.
    ** 021 CA  20140513 Added a weight for the context-local var, to ensure predetermined order during
    **                  context reset.
    ** 022 HC  20140629 Implemented access to the internal callee information structure.
    ** 023 CA  20140729 Made getRelativeName public.
    ** 024 VIG 20140810 Added invalid param processing for setCurrentWindow method.
    ** 025 CA  20140926 Added shared widget configuration support, to allow GUI/ChUI concrete
    **                  implementation for the same widget ID.  Refactored the widget configuration
    **                  classes: all fields were made public; these classes need to be as dumb as
    **                  possible, all logic related to setting/getting a certain field should be at
    **                  the widget or at the caller. Refs #2254
    ** 026 EVL 20141024 The dynamic flag need to be set early enough for dynamic widgets to avoid NPE
    **                  in processResource() method.
    ** 027 OM  20150112 Made _thisProcedure() public.
    ** 028 CA  20150306 When an external, non-persistent procedure, gets terminated, execute the same
    **                  work as when deleting a persistent procedure.
    ** 029 CA  20150501 DELETE OBJECT THIS-PROCEDURE forces the delete of the current procedure,
    **                  regardless of where is called (in an external proc block, inner proc, function
    **                  or trigger).
    **                  Track the resources created by each procedure, as dynamic resources don't get
    **                  deleted when the creator proc gets deleted, but their INSTANTIATING-PROCEDURE
    **                  attr needs to be unknown.
    ** 030 ECF 20150810 Removed redundant Throwable.fillInStackTrace call from resolveClosestMethod
    **                  as a performance improvement.
    ** 031 ECF 20150907 Made resolveClosestMethod public. Re-organized methods by access modifier.
    ** 032 CA  20151027 When running an internal entry (procedure, function) of a persistent procedure,
    **                  all its shared data (variables, tables, etc) needs to be pushed and popped,
    **                  so that the internal entry (and any other external program ran from it) has
    **                  access to it.
    **                  When a procedure gets deleted (persistent or not), then all created triggers
    **                  must be destroyed.
    **                  resolveInstantiatingProcedure must not use the handle's c'tor, as this will
    **                  not perform the assignment if a NO-ERROR clause is in effect.
    ** 033 ECF 20160105 Fixed concurrent modification errors in publish method.
    **     CA  20151219 Track the name of the external program being instantiated - required for
    **                  ERROR conditions raised during instantiation (i.e. by shared vars).
    **     OM  20161026 Added setDefaultWindow() method.
    ** 034 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 035 OM  20161026 Added cleanupPending() method.
    ** 036 IAS 20160314 DEBUG-ALERT support.
    ** 037 CA  20160418 Added infrastructure for message passing from converted code to a
    **                  customer-specific application or calls from the customer-specific application
    **                  into the converted code (i.e. invoke external programs, procedures or
    **                  functions).  Can be used only when the P2J client runs embedded in a
    **                  customer-specific application.
    ** 038 GES 20160630 Some ControlFlowOps cases call removeProcedureImpl() with a null handle, we
    **                  treat that as a no-op.
    ** 039 HC  20160718 PUBLISH/SUBSCRIBE runtime support modified so that events are fired in the
    **                  order of subscriptions. Procedure cleanup will also remove the procedure's
    **                  subscriptions.
    ** 040 CA  20160728 Fixed undoable support for data in external programs ran persistent.
    ** 041 HC  20160817 Fixed PUBLISH to properly invoke all procedures even in case there is a
    **                  failure due to procedure not found in the procedure file.
    **                  Fixed PUBLISH to resolve the target internal procedure in the subscribed
    **                  procedure as well as in its SUPER procedures or SESSION procedures.
    **                  Fixed UNSUBSCRIBE to only unsubscribe the procedure passed in the statement.
    ** 042 CA  20160812 Performance improvements for H040: use IdentityHashMap when using
    **                  ext prog instances as keys.
    ** 043 CA  20160824 Fixed a regression with delete-on-exit support: the external program must be
    **                  deleted only when the last of its internal entry on the stack is finished.
    ** 044 CA  20160902 Fixed H043 - only this-procedure stack is checked for delete-on-exit postpone.
    **                  Fixed a possible leak when deleting ext prog at scope finish.  Fixed order of
    **                  created static resources in an ext prog.
    ** 045 CA  20160907 For the duration of a PUBLISH call, the SOURCE-PROCEDURE must be set as the
    **                  publisher's procedure handle.
    ** 046 OM  20160901 Replaced ArrayDeque with custom Deque implementation for persistentProcedures.
    **                  Small optimizations.
    **     CA  20160923 Reworked H045 to explicitly set the PUBLISH's SOURCE-PROCEURE without
    **                  'hacking' the WorkArea.thisProcedures stack, as this is used in conjunction
    **                  with the WorkArea.internalEntryStack, when determining PROGRAM-NAME or
    **                  4GL-style stacktraces.
    ** 047 HC  20161110 Fixed an abend in publish() when subscriptions are removed as a result of the
    **                  published event processing.
    **                  Fixed several cases of referencing deleted system handles TARGET-PROCEDURE,
    **                  SOURCE-PROCEDURE and THIS-PROCEDURE.
    ** 048 CA  20170228 Added PUBLISH/SUBSCRIBE/UNSUBSCRIBE extensions for global support and for
    **                  external applications.
    ** 049 GES 20171206 Switch to ErrorManager.silent().
    ** 050 CA  20171202 Added searchInStack.
    ** 051 ECF 20171228 Refactored to remove redundant context-local lookups where possible.
    ** 052 CA  20181112 Added support for the TRANSACTION-MODE AUTOMATIC [CHAINED] language statement.
    **                  Added support for the TRANSACTION resource.
    ** 053 EVL 20190219 Making hasReferent() method to be public to call outside the class.
    **     ECF 20190306 Replaced stream filter loop with simple for loop for better performance in
    **                  deleteResources.
    ** 054 CA  20190123 Definitions like FUNCTION ... IN hvar will not convert via a HandleExpr inner
    **                  class, but will receive the handle variable instead and the runtime will wrap
    **                  it into a Resolvable.
    **                  Infrastructure for class event registration.
    **     CA  20190219 Added runtime for class events.
    **                  Added runtime for BLOCK-LEVEL UNDO, THROW and ROUTINE-LEVEL UNDO, THROW.
    **     CA  20190325 Refactored to expose heavy-duty APIs via a instance which provides direct
    **                  access to the context-local state.
    **     CA  20190326 Implemented TransactionHelper instead of direct usage of the static API.
    **                  This allows the elimination of context local usage in TM.
    **                  WidgetPool's scope support is now exposed via a helper.
    ** 055 CA  20190628 Added SINGLE-RUN and SINGLETON support.
    ** 056 OM  20190718 Avoid NPE on hand-written code.
    **     CA  20190719 Allow searchInStack via ProcedureHelper.
    **     CA  20190720 Load implicit default databases - these databases are for non-default schemas
    **                  in p2j.cfg.xml, so that they will not be loaded during normal conversion
    **                  unless a hint is specified.  We emulate this in the directory, in the
    **                  'default-databases' section, where the databases as specified for each Java
    **                  package prefix.
    **     HC  20190727 Dynamic function lookup must be case insensitive.
    **     CA  20190816 Static temp-tables are now created when their default buffer is defined.
    **     CA  20190903 Added GLOBAL support for SUBSCRIBE/UNSUBSCRIBE (cross-session messaging).
    ** 057 AIL 20191120 Made the scope methods aware of DATABASE TRIGGER DataType
    **     CA  20191119 Track the executing legacy class, to be able to register any buffer to its
    **                  source definition class.
    **     CA  20200110 Fixed for resource deleted, in cases of OE class hierarchy and REFERENCE-ONLY
    **                  parameters.
    ** 058 CA  20200304 A 4GL object instance must delay its delete if is still on stack.
    ** 059 CA  20200427 For a remote appserver request, the INSTANTIATING-PROCEDURE may be null for
    **                  a persistently run program.
    ** 060 CA  20200720 Moved some APIs to ProcedureHelper.
    ** 061 AIL 20200616 Added the persistentProcStacks, which should store snapshots of block stacks
    **                  and are used to properly delete persistent procedures (defer if needed).
    **         20200617 Added support for stale persistent procedures handling.
    **         20200622 Changed the signature of procedure delete in order to exploit the stale
    **                  persistent procedure by default.
    **                  Impovements and javadoc update.
    **     GES 20200804 Removed context-local lookups from some ErrorManager usage.
    **     CA  20200927 Avoid context-local lookups by relying on the helper instance.
    **     CA  20200930 More context-local lookup improvements.
    **     RFB 20201002 Added a new thisProcedure, which allows for a referent to be passed in. Ref #4861
    **     CA  20201003 Use an identity HashSet where possible.
    **     CA  20201015 Replaced java.util.Stack with a non-synchronized custom implementation.
    **     RFB 20201015 New thisProcedure should handle null returned from locateProcedure. Ref. 4936.
    **     CA  20201119 The parameters for a PUBLISH call must not be altered by the ControlFlowOps invoke (in case
    **                  of OUTPUT or INPUT-OUTPUT, it ensures to use a different instance, to not alter the
    **                  caller's reference).  For this reason, PUBLISH ensures that after a call, all parameter
    **                  references are re-established and the values updated.
    **     CA  20201201 Performance improvement - keep the subscribers in a 'per event' map, so that PUBLISH will
    **                  avoid iterating all subscriptions, and work only with the required event.
    **     CA  20210104 Fixed a regression after adding BaseObject.equals/hashCode - replaced plain set with
    **                  identity set and a bug in resource delete.
    **     CA  20210310 Performance improvement for management of super-procedures (keep track of where each
    **                  referent was added as a super-proc, so it knows which super-procedures to clean when it
    **                  gets deleted, instead of walking the entire procedure chain).
    **     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.
    **     CA  20210910 Fixed a NPE when a legacy OO instance is deleted from within a method invocation on its
    **                  own instance.
    **     CA  20210917 Javadoc fixes.
    **     HC  20211001 Implementation of i18n support.
    **     OM  20211005 Added procedure-cleanup handler support. Object can register with a piece of code to be
    **                  executed on procedure return (like table parameter unbinding).
    **     AL2 20211015 Support interation over the stack of resolved destinations of calls.
    **     ECF 20211124 Added isExecuted method to report whether an external procedure has been executed.
    **     CA  20211222 Legacy OO does not have 'stale procedure' behavior - this is specific only for external
    **                  programs, and not legacy classes.
    **     CA  20220104 CA/20211222 needs to include the 'static class' referent, too, not just legacy instances.
    **     CA  20220106 PUBLISH arguments must be evaluated only if the subscription is found.
    **     CA  20220107 Fixed a NPE for remote PUBLISH (from JS client).
    **     CA  20220120 Performance improvement for pushing a new calee scope, keep the already resolved internal
    **                  entry legacy name.
    **     CA  20220203 Added 'currentInternalEntry', to get the name of the currently executing internal entry.
    **     CA  20220208 Added SESSION:ERROR-STACK-TRACE runtime support.
    **     TW  20220803 Error-status:error must be set for return error in method, while return error
    **                  in old-style ABL function must remain the same. Refs: #6567.
    **     TW  20220806 Error-status:error must be set for return error in method. Javadoc fix.
    **     TW  20220817 Error-status:error must be set for return error in static method.
    **     CA  20220930 Refactored the callback invocation to be performed via a call-site and InvokeConfig, to
    **                  allow caching of the resolved target.  ProcedureManager is responsible for invalidating
    **                  the cache when the SUPER-PROCEDURES change or an external program gets deleted.
    **     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  20221006 Improved performance of pending resources.  Refs #6824
    **                  UNIQUE-ID is kept as Java type instead of BDT.  Refs #6827
    **     CA  20220427 Fixed isProxy when being called outside of a FWD context.
    **     CA  20220526 If there are subscriptions for a legacy OO instance, do not delete (implicitly).
    **     CA  20220526 Track and report any errors when deleting a persistent program (i.e. from buffer
    **                  validation).
    **     CA  20220613 Improvement for the 'execute on return' handlers - process them only for top-level blocks.
    **     CA  20220918 Use a weak reference for the INSTANTIATING-PROCEDURE, to allow the garbage collector to
    **                  remove the references once this was deleted.
    **     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 Moved more APIs to ProcedureHelper, to reduce ContextLocal.get calls.
    **                  UNIQUE-ID is generated only when is read by the application.
    **     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
    **                  Improvement from context-local state resolution.
    ** 062 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 063 CA  20230605 Made setInstantingExternalProgram public.
    ** 064 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.
    ** 065 AL2 20230613 Make executeOnReturn, disablePendingResources, enablePendingResources available in helper
    ** 066 CA  20230703 Allow deregistration of the 'executeOnReturn' cleaners.
    ** 067 CA  20230712 Added '_rootProcedure', to retrieve the root procedure instance.
    ** 068 CA  20230724 Further reduce context-local usage.
    ** 069 SVL 20230810 Do not include persistent trigger procedure in the legacy stack trace.
    ** 070 CA  20230818 Fixed a regression in H068 - when obtain() is used, we need to explicitly execute the
    **                  'WorkArea.init' code.
    ** 071 CA  20231009 Fixed the 'executing' type for direct Java calls.
    ** 072 CA  20231026 A persistent procedure with a pending delete is no longer persistent, so scopeDeleted
    **                  needs to be executed.
    **     CA  20231031 Added 'BlockDefinition' parameter to 'scopeStart'.
    ** 073 CA  20231031 The constructor and destructor 'internal entry name' is the exact legacy name for this
    **                  entry, as it is reported by PROGRAM-NAME and the error callstack.
    ** 074 AI  20231027 Added executeInThisProcedure() to execute a block inside the given procedure as the
    **                  new THIS-PROCEDURE.
    **     AI  20231031 Pushed into internalEntryStack from executeInThisProcedure() to mark the newly added
    **                  THIS-PROCEDURE to be excluded from the stack-trace. Added THIS_PROCEDURE_OVERRIDE
    **                  constant to be pushed into internalEntryStack().
    ** 075 CA  20231129 Improvements related to the management of various dequeues and collections (use empty
    **                  set/map instead of creating a new instance, where possible).
    **     CA  20231221 Refactored the WidgetPool scopeable support to register for processing as needed.
    **                  Refactored the this/source/target-procedure stacks, to remain only one for each case.
    ** 076 CA  20240105 Fixed regression in H075 - running an internal entry in a persistent procedure which
    **                  has an unnamed pool must push WidgetPool scope for this internal entry.
    ** 077 CA  20240206 Added 'onStackReferences'.
    ** 078 VVT 20240326 buildTraceLine(): adjust for changes in populateCallStack(): pass all source data as
    **                  arguments. See #8130.
    ** 079 SC  20240508 Added flag and checking implementation for dynamic calls in CalleeInfoImpl
    **                  Added method overload for pushCalleeInfo to cover the checking for dynamic calls
    **     SC  20240516 Refactored the body of overloaded method pushCalleeInfo
    ** 080 TJD 20240126 Java 17 fixes, c.g.p2j.net.Queue must be public
    ** 081 CA  20240809 Avoid iterator usage, for performance improvement.
    ** 082 VVT 20240826 SourceNameMapper.convertJavaProg() renamed. See #8613-16.
    ** 083 AP  20240827 Improved firstProcedure to only use the iterator instead of accessing the HashMap from
    **                  FastSiblingDeque for each node.
    ** 084 AL2 20240930 Use identity hash map for FastSiblingDeque.
    ** 085 ICP 20250129 Used logical constant to leverage cached values, fixed javadoc.
    ** 086 CA  20250221 For delayed delete, save/restore the 'nowIterating' set, same as in
    **                  ControlFlowOps.executeDestructor.
    ** 087 CA  20250311 Fixed virtual function support defined in 4GL classes, with static property/var as
    **                  expression.
    ** 088 OM  20250131 Fine-tuned exit condition from processResource() in headless mode. Made load() public.
    ** 089 GBB 20250403 wa.tm can be null in multi-session appservers. deleteInstance made public.
    */

    /*
    ** 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.lang.ref.*;
    import java.lang.reflect.*;
    import java.util.*;
    import java.util.Queue;
    import java.util.function.*;
    import java.util.logging.*;

    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.scheduler.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.ui.client.event.ServerEvent;
    import com.goldencode.p2j.util.UniqueIdGenerator.*;
    import com.goldencode.p2j.util.Utils.DirScope;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.util.*;
    import com.goldencode.util.Stack;

    import static com.goldencode.p2j.util.ErrorManager.*;

    /**
     * Provides implementation for the following procedure handles:
     * <table summary="">
     * <tr>
     *   <td>THIS-PROCEDURE</td>
     *   <td>{@link #thisProcedure()}</td>
     *   <td>{@link #thisProcedure(Object)}</td>
     * </tr>
     * <tr>
     *   <td>SOURCE-PROCEDURE</td>
     *   <td>{@link #sourceProcedure()}</td>
     * </tr>
     * <tr>
     *   <td>TARGET-PROCEDURE</td>
     *   <td>{@link #targetProcedure()}</td>
     * </tr>
     * </table>
     * The following procedure-related attributes and methods are implemented:
     * <table summary="">
     * <tr>
     *    <th>attribute or method</th>
     *    <th>API</th>
     * </tr>
     * <tr>
     *    <td>SESSION:SUPER-PROCEDURES</td>
     *    <td>{@link #superProcedures()}</td>
     * </tr>
     * <tr>
     *    <td>SESSION:ADD-SUPER-PROCEDURE</td>
     *    <td>
     *       <ul>
     *          <li>{@link #addSuperProcedure(handle)}</li>
     *          <li>{@link #addSuperProcedure(handle, SearchMode)}</li>
     *          <li>{@link #addSuperProcedure(handle, int)}</li>
     *       </ul>
     *    </td>
     * </tr>
     * <tr>
     *    <td>SESSION:REMOVE-SUPER-PROCEDURE</td>
     *    <td>
     *       <ul>
     *          <li>{@link #removeSuperProcedure(handle)}</li>
     *       </ul>
     *    </td>
     * </tr>
     * <tr>
     *    <td>SESSION:FIRST-PROCEDURE</td>
     *    <td>{@link #firstProcedure()}</td>
     * </tr>
     * <tr>
     *    <td>SESSION:LAST-PROCEDURE</td>
     *    <td>{@link #lastProcedure()}</td>
     * </tr>
     * </table>
     * <p>
     * The {@link #registerFunctionHandle(String, Resolvable)} is emitted at the
     * beginning of an external procedure, for any function defined
     * <code>IN handle</code>.
     */
    public class ProcedureManager
    {
       /** Constant defining search mode: SEARCH-SELF */
       public static final int SEARCH_SELF = 1;

       /** Constant for socket search mode: SEARCH_TARGET */
       public static final int SEARCH_TARGET = 2;

       /** Marks the external program's root procedure - this is a P2J reserved name. */
       public static final String EXTERNAL_PROGRAM = "__P2J__EXTERNAL__PROGRAM__";

       /** Marks the trigger block - this is what 4GL reports for triggers. */
       public static final String TRIGGER_PROCEDURE = "USER-INTERFACE-TRIGGER";

       /**
        * Marks the persistent trigger block (ON ... PERSISTENT RUN) - FWD-only, doesn't exist in 4GL.
        */
       public static final String PERSISTENT_TRIGGER_PROCEDURE = "PERSISTENT_TRIGGER_PROCEDURE";

       /** Marks a temporary THIS-PROCEDURE - FWD-only, does not exust in 4GL. */
       public static final String THIS_PROCEDURE_OVERRIDE = "THIS_PROCEDURE_OVERRIDE";

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(ProcedureManager.class.getName());

       /** Worker for managing global subscriptions. */
       private static GlobalEventSubscriptions globalSubscriptions;

       /**
        * Defines search modes for the ADD-SUPER-PROCEDURE statement. Needed to
        * be able to identify the correct target in RUN SUPER and SUPER() cases.
        */
       public static enum SearchMode
       {
          SEARCH_SELF
          {
             @Override
             public String toString()
             {
                return "SEARCH-SELF";
             }
          },

          SEARCH_TARGET
          {
             @Override
             public String toString()
             {
                return "SEARCH-TARGET-PROCEDURE";
             }
          }
       }

       /** Stores context-local state variables. */
       private static ContextContainer work = new ContextContainer();

       /**
        * API executed on client startup, to initialize this class.
        */
       public static void initialize()
       {
          // WARNING: do not initialize static fields in the default class c'tor, as the
          // ProcedureManager currently gets resolved on client-side, too, because of a client-side
          // dependency on TM.setNestedMode

          // initialize the static fields.
          globalSubscriptions = new GlobalEventSubscriptions();
       }

       /**
        * Register the handle variable as a resolvable for the given function.
        *
        * @param    pname
        *           The function's legacy 4GL name.
        * @param    hvar
        *           The handle variable which determins the function's target.
        */
       public static void registerFunctionHandle(String pname, handle hvar)
       {
          registerFunctionHandle(pname, new Resolvable()
          {
             @Override
             public BaseDataType resolve()
             {
                return hvar;
             }

             @Override
             public Class getType()
             {
                return handle.class;
             }
          });
       }

       /**
        * Register the handle expression for the given function.
        *
        * @param    pname
        *           The function's legacy 4GL name.
        * @param    handleExpr
        *           The {@link Resolvable} instance used to determine its IN
        *           handle target.
        */
       public static void registerFunctionHandle(String pname, Resolvable handleExpr)
       {
          // always work with thisProcedure
          handle h = thisProcedure();
          Object referent = h.get();
          ProcedureData pp = locateProcedure(referent);
          if (pp.inHandleFunctions == Collections.EMPTY_MAP)
          {
             pp.inHandleFunctions = new HashMap<>();
          }
          pp.inHandleFunctions.put(pname.toLowerCase(), handleExpr);
       }

       /**
        * Implementation of the TRANSACTION-MODE AUTOMATIC language statement.
        */
       public static void transactionModeAuto()
       {
          transactionModeAuto(false);
       }

       /**
        * Implementation of the TRANSACTION-MODE AUTOMATIC [CHAINED] language statement.
        *
        * @param    chained
        *           Flag indicating that the CHAINED option is set.
        */
       public static void transactionModeAuto(boolean chained)
       {
          WorkArea wa = work.obtain();

          // we must be remote and the first accessed procedure - otherwise, fail
          if (!(AppServerManager.isRemote() && wa.tm.isRootNestingLevel()))
          {
             String pname = ProcedureManager.getAbsoluteName(_thisProcedure(wa));
             ErrorManager.recordOrThrowError(7335,
                                             "AUTOMATIC MODE TRANSACTION procedure " + pname +
                                             " may only be run remotely as a top-level procedure");
             return;
          }

          if (!TransactionImpl.get().getTxInitProcedure().isUnknown() || wa.tm.isTransaction())
          {
             ErrorManager.recordOrThrowError(7336,
                                             "A .p with TRANSACTION-MODE AUTOMATIC may not be run " +
                                             "inside a transaction or while another AUTOMATIC .p " +
                                             "is running in the same process",
                                             false);
             return;
          }

          BufferManager.registerScopeable(BufferManager.get());

          Object referent = _thisProcedure(wa);
          ProcedureData pp = locateProcedure(wa, referent);

          pp.transactionModeAutoChained = chained;

          // set TRANS-INIT-PROCEDURE to THIS-PROCEDURE
          TransactionImpl.setTxInitProcedure(referent);

          // start the transaction
          wa.tm.beginTx();
       }

       /**
        * Get the THIS-PROCEDURE system handle.
        *
        * @return   See above.
        */
       public static handle thisProcedure()
       {
          return thisProcedure(work.obtain());
       }

       /**
        * Get the THIS-PROCEDURE system handle.
        *
        * @param    ref
        *           Reference handle
        *
        * @return   See above.
        */
       public static handle thisProcedure(Object ref)
       {
          ProcedureData referent = locateProcedure(ref);

          return (referent != null) ? new handle(referent.phandle) : new handle();
       }

       /**
        * Get the TARGET-PROCEDURE system handle.
        *
        * @return   See above.
        */
       public static handle targetProcedure()
       {
          return targetProcedure(work.obtain());
       }

       /**
        * Get the SOURCE-PROCEDURE system handle.
        *
        * @return   See above.
        */
       public static handle sourceProcedure()
       {
          WorkArea wa = work.obtain();
          ProcedureData pd = wa.sourceProceduresData.peek();

          // Do not use locateProcedure() here, the system handle may have been deleted
          // with DELETE PROCEDURE. Note that when a procedure is deleted it can be
          // referenced and queried through the system procedure handles.
          return pd == null ? new handle() : new handle(pd.phandle);
       }

       /**
        * Get the SESSION:LAST-PROCEDURE handle (unknown if no persistent
        * procedures are loaded).
        *
        * @return   See above.
        */
       public static handle lastProcedure()
       {
          WorkArea wa = work.obtain();
          if (wa.persistentProcedures.isEmpty())
          {
             return new handle();
          }

          Object referent = wa.persistentProcedures.peekLast();
          while (ObjectOps.isLegacyObject(referent))
          {
             referent = wa.persistentProcedures.getPrevious(referent);
          }
          if (referent == null)
          {
             return new handle();
          }

          return new handle(locateProcedure(wa, referent).phandle);
       }

       /**
        * Get the SESSION:FIRST-PROCEDURE handle (unknown if no persistent
        * procedures are loaded).
        *
        * @return   See above.
        */
       public static handle firstProcedure()
       {
          WorkArea wa = work.obtain();
          if (wa.persistentProcedures.isEmpty())
          {
             return new handle();
          }

          // find the first persistentProcedure which is not a LegacyObject
          for (Object referent : wa.persistentProcedures)
          {
             if (!ObjectOps.isLegacyObject(referent))
             {
                return new handle(locateProcedure(wa, referent).phandle);
             }
          }

          return new handle();
       }

       /**
        * Get the SESSION:SUPER-PROCEDURES attribute.
        *
        * @return   See above.
        */
       public static character superProcedures()
       {
          WorkArea wa = work.obtain();
          return superProceduresImpl(wa.getSessionSuperProcedures().values());
       }

       /**
        * Add the given handle as a super-procedure for the SESSION handle.
        *
        * @param    h
        *           The handle to be added as a super-procedure.
        *
        * @return   <code>true</code> if <code>h</code> is a valid procedure
        *           handle.
        */
       public static logical addSuperProcedure(handle h)
       {
          return addSuperProcedure(h, (SearchMode) null);
       }

       /**
        * Add the given handle as a super-procedure for the SESSION handle, using
        * the explicit {@link SearchMode mode}.
        *
        * @param    h
        *           The handle to be added as a super-procedure.
        * @param    searchMode
        *           The {@link SearchMode} to be set for the <code>h</code>
        *           handle.
        *
        * @return   <code>true</code> if <code>h</code> is a valid procedure
        *           handle.
        */
       public static logical addSuperProcedure(handle h, int searchMode)
       {
          return addSuperProcedure(h, getSearchMode(searchMode));
       }

       /**
        * Add the given handle as a super-procedure for the SESSION handle, using
        * the explicit {@link SearchMode mode}.
        *
        * @param    h
        *           The handle to be added as a super-procedure.
        * @param    searchMode
        *           The {@link SearchMode} to be set for the <code>h</code>
        *           handle.
        *
        * @return   <code>true</code> if <code>h</code> is a valid procedure
        *           handle.
        */
       public static logical addSuperProcedure(handle h, integer searchMode)
       {
          if (searchMode.isUnknown())
          {
             return addSuperProcedure(h, (SearchMode) null);
          }
          else
          {
             return addSuperProcedure(h, searchMode.intValue());
          }
       }

       /**
        * Add the given handle as a super-procedure for the SESSION handle, using
        * the search-mode set as a string (one of the <code>SEARCH-TARGET</code>
        * or <code>SEARCH-SELF</code> string literals).
        *
        * @param    h
        *           The handle to be added as a super-procedure.
        * @param    mode
        *           The search-mode to be set for the <code>h</code> handle.
        *
        * @return   <code>true</code> if <code>h</code> is a valid procedure
        *           handle.
        */
       public static logical addSuperProcedure(handle h, SearchMode mode)
       {
          return addSuperProcedureImpl(work.obtain(), h, mode, null);
       }

       /**
        * Remove the given handle from the SESSION's list of super-procedures.
        *
        * @param    h
        *           The procedure handle to be removed.
        *
        * @return   <code>true</code> if <code>h</code> is a valid procedure
        *           handle.
        */
       public static logical removeSuperProcedure(handle h)
       {
          if (!h._isValid() || !isProcedure(h))
          {
             return new logical(false);
          }

          ControlFlowOps.invalidateCallSiteCache();

          WorkArea wa = work.obtain();
          removeProcedureImpl(wa, h, wa.getSessionSuperProcedures().keySet());
          ProcedureData pData = locateProcedure(wa, h.get());
          pData.isSessionSuperProcedure = false;

          return new logical(true);
       }

       /**
        * Process the given resource by either setting its INSTANTIATING-PROCEDURE attribute
        * immediately (in case of non-static resources) or add the static resource to the
        * {@link WorkArea#pendingResources} set, to be processed when the next scope is entered.
        *
        * @param    res
        *           The resource to be processed.
        * @param    dynamic
        *           Flag indicating if this is a dynamic or static resource.
        */
       public static void processResource(WrappedResource res, boolean dynamic)
       {
          processResource(work.obtain(), res, dynamic);
       }

       /**
        * Process the given resource by either setting its INSTANTIATING-PROCEDURE attribute
        * immediately (in case of non-static resources) or add the static resource to the
        * {@link WorkArea#pendingResources} set, to be processed when the next scope is entered.
        *
        * @param    wa
        *           The context-local state.
        * @param    res
        *           The resource to be processed.
        * @param    dynamic
        *           Flag indicating if this is a dynamic or static resource.
        */
       public static void processResource(WorkArea wa, WrappedResource res, boolean dynamic)
       {
          // be a no-op in case of headless mode
          if (wa.tm == null || wa.tm.errHlp != null && wa.tm.errHlp.isHeadless())
          {
             return;
          }

          if (!wa.registerPendingResources)
          {
             return;
          }

          if (res instanceof StaticTempTable || (res instanceof DynamicResource && !dynamic))
          {
             // all static resources are created as instance fields; thus, the actual processing is
             // delayed, as the THIS-PROCEDURE is not available at the time of the call (i.e. we are
             // in an instance field initialization code, at the business logic class)
             wa.pendingResources.add(res);
          }
          else if (res instanceof HandleResource)
          {
             wa.pendingResources.remove(res);

             // all resources which don't have dynamic counterparts are processed immediately
             Object referent = _thisProcedure(wa);
             ProcedureData pdata = locateProcedure(wa, referent);

             // need to init dynamic flag here to avoid NPE
             if (dynamic && res instanceof GenericWidget)
             {
                ((GenericWidget)res)._setDynamic(dynamic);
             }

             // [pdata] should always be not-null in normal usage, except when the resource is
             // created outside the FWD environment, like handle-written code.
             if (pdata != null)
             {
                ((HandleResource) res).setInstantiatingProcedure(pdata.phandle);
             }
          }
       }

       /**
        * The {@code ProcedureManager} should know the DEFAULT-WINDOW in order to prevent releasing /
        * deleting it when the top-level scope is popped out.
        * <p>
        * The DEFAULT-WINDOW was eventually added in {@link #processResource(WrappedResource,
        * boolean)} but, at that time, we were not able to distinct it. The {@link LogicalTerminal}
        * must call this as soon as the internal variable has been set.
        *
        * @param   defaultWindow
        *          The default window for this session.
        */
       public static void deregisterPendingResource(WindowWidget defaultWindow)
       {
          WorkArea wa = work.obtain();

          // a no-op in case of headless mode
          if (wa.tm.errHlp.isHeadless())
          {
             return;
          }

          wa.pendingResources.remove(defaultWindow);
       }

       /**
        * Get the current external program (an application logic instance) being processed for
        * scopeable or finalizable notifications.
        *
        * @return   See above
        */
       public static Object getProcessedProcedure()
       {
          WorkArea wa = work.obtain();

          return wa.processedProcedure;
       }

       /**
        * Check if the given referent is for a persistent procedure. This implements the PERSISTENT
        * attribute for a procedure handle.
        *
        * @param    referent
        *           An instance of a converted external procedure class.
        *
        * @return   <code>true</code> if the referent is a persistent procedure.
        */
       public static boolean _isPersistent(Object referent)
       {
          return _isPersistent(work.obtain(), referent);
       }

       /**
        * Deregister an external program which was ran persistent by a remote side.
        *
        * @param   referent
        *          The external program instance.
        */
       public static void deregisterRemotePersistentProcedure(Object referent)
       {
          work.obtain().remotePersistentProcedures.remove(referent);
       }

       /**
        * Check if an external program was ran persistent by a remote side.
        *
        * @param   referent
        *          The external program instance.
        *
        * @return  <code>true</code> if the referent is part of the
        *          {@link WorkArea#remotePersistentProcedures} map.
        */
       public static boolean isRemotePersistentProcedure(Object referent)
       {
          return work.obtain().isRemotePersistentProcedure(referent);
       }

       /**
        * Check if the handle's referent is a procedure or not. This means that the value must be set
        * and must be an instance of {@link ExternalProgramWrapper}. The enclosed referent may or may
        * not be registered with the {@link ProcedureManager}.
        *
        * @param    h
        *           handle object to check.
        *
        * @return   See above.
        */
       public static boolean isProcedure(handle h)
       {
          return h != null && !h.isUnknown() && (h.getResource() instanceof ExternalProgramWrapper);
       }

       /**
        * Gets the value of CURRENT-WINDOW attribute of specified procedure
        * @param   referent
        *          Procedure wrapper
        * @return  handle.
        */
       public static handle getCurrentWindow(Object referent)
       {
          ProcedureData pd = locateProcedure(referent);
          return pd.currentWindow == null ? new handle(): new handle(pd.currentWindow);
       }

       /**
        * Sets the value of CURRENT-WINDOW attribute of specified procedure
        * @param   referent
        *          Procedure wrapper
        * @param   winHandle
        *          New value for the attribute.
        */
       public static void setCurrentWindow(Object referent, handle winHandle)
       {
          if (winHandle == null || !winHandle._isValid() || winHandle.isUnknown())
          {
             ErrorManager.recordOrShowError(
                4083,
                "Unable to assign UNKNOWN value to attribute CURRENT-WINDOW on PROCEDURE widget",
                false, false, false);

             return;
          }

          if (!(winHandle.getResource() instanceof WindowWidget))
          {
             String ms = "Unable to set attribute CURRENT-WINDOW of procedure widget";
             ErrorManager.recordOrShowError(4078, ms, false, false, false);

             return;
          }

          ProcedureData pd = locateProcedure(referent);
          pd.currentWindow = winHandle.getResource();
       }

       /**
        * Get the relative name used to create the given external program instance.
        *
        * @param    instance
        *           The external program instance.
        *
        * @return   See above.
        */
       public static String getRelativeName(Object instance)
       {
          return getRelativeName(work.obtain(), instance);
       }

       /**
        * Resolve the closest business logic method in the stack trace.
        *
        * @param    referent
        *           The external program to which the method must belong.
        * @param    function
        *           Flag indicating if a function or internal procedure will be searched.
        * @param    externalProc
        *           Flag indicating if we are looking for an external procedure.
        *
        * @return   The legacy method name.
        */
       public static String resolveClosestMethod(Object referent,
                                                 boolean function,
                                                 boolean externalProc)
       {
          // get a helper to process the stack
          Throwable stack = new Throwable();

          // obtain the stack trace
          StackTraceElement[] trace = stack.getStackTrace();

          String rClsName   = referent.getClass().getName();
          String methodName = null;
          String legacyName = null;

          String bmClass = BlockManager.class.getName();
          String procApi = (externalProc ? "internalProcedure" : "externalProcedure");
          String funcApi = "function";

          // walk the stack trace until we find a method defined by the referent's class
          boolean foundBlockManager = false;
          for (int i = 1; i < trace.length; i++)
          {
             String cname = trace[i - 1].getClassName();
             String mname = trace[i - 1].getMethodName();

             if (cname.equals(bmClass) &&
                 ((function && mname.toLowerCase().endsWith(funcApi)) ||
                  (!function && mname.equals(procApi))))
             {
                foundBlockManager = true;
             }
             else if (foundBlockManager)
             {
                methodName = mname;
                break;
             }
          }

          if (methodName != null)
          {
             if ("execute".equals(methodName))
             {
                // this is a special 4GL string.
                legacyName = EXTERNAL_PROGRAM;
             }
             else
             {
                // resolve the legacy name of this method
                legacyName = SourceNameMapper.convertJavaMethod(rClsName, methodName, function);

                if (legacyName == null)
                {
                   // assume the java name as legacy name
                   legacyName = methodName;
                }
             }
          }

          if (legacyName == null)
          {
             final String msg = "Could not resolve closest legacy method for class %s";
             throw new RuntimeException(String.format(msg, rClsName) , stack);
          }

          return legacyName;
       }

       /**
        * Get the name of the currently instantiated external program.
        *
        * @return   The 4GL external program name being instantiated, or <code>null</code> if this is
        *           not the case.
        */
       public static String getInstantiatingExternalProgram()
       {
          WorkArea wa = work.obtain();
          return wa.initiatingExtProg;
       }

       /**
        * Get the source class of the currently instantiating external program or OE class.
        *
        * @return   The Java class associated with the source code for the current instantiating OE
        *           class.
        */
       public static Class<?> getInstantiatingExternalProgramClass()
       {
          WorkArea wa = work.obtain();
          return wa.initiatingExtProgClass;
       }

       /**
        * Set the Java class of the currently instantiating external program or OE class.
        *
        * @param    cls
        *           The Java class for the currently executing code.
        */
       public static void setInstantingExternalProgramClass(Class<?> cls)
       {
          work.obtain().setInstantingExternalProgramClass(cls);
       }

       /**
        * Get an instance of {@link ProcedureHelper}, which has the context-local instance saved,
        * so API access can be done without a context-local query.
        *
        * @return   See above.
        */
       public static ProcedureHelper getProcedureHelper()
       {
          return new ProcedureHelper(work.obtain());
       }

       /**
        * Get the default databases visible in the current stack.
        *
        * @return   The nearest non-null {@link ProcedureData#connectedDbs} in the stack.
        */
       public static Set<String> getDefaultDatabases()
       {
          WorkArea wa = work.obtain();
          if (wa.defaultDbs == null)
          {
             return null;
          }

          Set<String> dbs = (Set<String>) wa.searchInStack((referent) ->
          {
             ProcedureData pdata = locateProcedure(wa, referent);
             return pdata.connectedDbs;
          });

          return dbs;
       }

       /**
        * Get the singleton instance of the specified class.  This can be a 4GL external program or
        * legacy class.
        *
        * @param    cls
        *           The converted Java class for which the singleton instance is required.
        *
        * @return   See above.
        */
       static Object getSingleton(Class<?> cls)
       {
          WorkArea wa = work.obtain();

          return wa.singletonMap.get(cls);
       }

       /**
        * Save a singleton instance for the given class.
        *
        * @param    cls
        *           The class.
        * @param    instance
        *           The singleton instance.
        */
       static void addSingleton(Class<?> cls, Object instance)
       {
          WorkArea wa = work.obtain();

          wa.singletonMap.put(cls, instance);
       }

       /**
        * Delete the specified instance.
        *
        * @param    hprog
        *           A handle referencing either a external program or {@link _BaseObject_} instance.
        * @param    isClass
        *           Flag indicating if the handle is referencing a class.
        */
       public static void deleteInstance(handle hprog, boolean isClass)
       {
          if (hprog._isValid())
          {
             if (isClass)
             {
                ObjectOps.delete((_BaseObject_) hprog.get());
             }
             else
             {
                HandleOps.delete(hprog);
             }
          }
       }

       /**
        * Get the {@link Scopeable} instance for this context.
        *
        * @return   See above.
        */
       static Scopeable getScopeable()
       {
          return work.obtain();
       }

       /**
        * Set the external procedure beeing processed for scopeable or finalizable notifications.
        *
        * @param    referent
        *           The external procedure instance.
        */
       static void setProcessedProcedure(Object referent)
       {
          WorkArea wa = work.obtain();

          wa.processedProcedure = referent;
       }

       /**
        * Notify all subscribers (to the given event) that the event has occurred. This method is
        * executed in a {@link ErrorManager#silent} bracket, as the <code>PUBLISH</code>
        * statement is executed with a default NO-ERROR clause.
        * <p>
        * All processing is done with silent error mode implicitly enabled.
        * <p>
        * The subscriptions are fired in the order they were subscribed. Although 4GL
        * documentation specifically states, that "If a named event has multiple subscribers,
        * the order in which the AVM notifies subscribers is undefined.", the actual
        * 4GL implementation (OpenEdge 10.2B) does fire the events in the order the subscriptions
        * get subscribed.
        *
        * @param    classEvent
        *           Flag indicating if we are publishing a class event.
        * @param    event
        *           The name of the event which is being published.
        * @param    publisher
        *           The procedure handle representing the program that is the source of the event.
        *           Must not be <code>null</code>.
        * @param    modes
        *           The mode for each parameter, encoded in a string. When <code>null</code>,
        *           the parameter modes will not be checked against the procedure definition.
        * @param    parameters
        *           The parameters passed to the subscriber. May be <code>null</code> to represent no
        *           parameters. When <code>null</code>, it means that no parameters were specified for
        *           this call.
        */
       static void publish(boolean            classEvent,
                           character          event,
                           handle             publisher,
                           String             modes,
                           Supplier<Object[]> parameters)
       {
          publish(classEvent, false, event, publisher, modes, parameters);
       }

       /**
        * Notify all subscribers (to the given event) that the event has occurred. This method is
        * executed in a {@link ErrorManager#silent} bracket, as the <code>PUBLISH</code>
        * statement is executed with a default NO-ERROR clause.
        * <p>
        * All processing is done with silent error mode implicitly enabled.
        * <p>
        * The subscriptions are fired in the order they were subscribed. Although 4GL
        * documentation specifically states, that "If a named event has multiple subscribers,
        * the order in which the AVM notifies subscribers is undefined.", the actual
        * 4GL implementation (OpenEdge 10.2B) does fire the events in the order the subscriptions
        * get subscribed.
        *
        * @param    classEvent
        *           Flag indicating if we are publishing a class event.
        * @param    global
        *           Flag indicating if the publish is for global (cross-session) subscriptions, too.
        * @param    event
        *           The name of the event which is being published.
        * @param    publisher
        *           The procedure handle representing the program that is the source of the event.
        *           Must not be <code>null</code>.
        * @param    modes
        *           The mode for each parameter, encoded in a string. When <code>null</code>,
        *           the parameter modes will not be checked against the procedure definition.
        * @param    parameters
        *           The parameters passed to the subscriber. May be <code>null</code> to represent no
        *           parameters. When <code>null</code>, it means that no parameters were specified for
        *           this call.
        */
       static void publish(boolean            classEvent,
                           boolean            global,
                           character          event,
                           handle             publisher,
                           String             modes,
                           Supplier<Object[]> parameters)
       {
          Runnable ptask = () ->
          {
             if (!publisher.isExternalResource())
             {
                if (publisher.isUnknown())
                {
                   final String msg = "Could not evaluate publisher handle expression while trying " +
                                      "to execute PUBLISH statement";
                   ErrorManager.recordOrThrowError(6472, msg, false);
                   return;
                }
                if (!isProcedure(publisher) || !publisher._isValid())
                {
                   final String msg =
                      "Invalid or inappropriate publisher handle value given to PUBLISH statement";
                   ErrorManager.recordOrThrowError(9072, msg, false);
                   return;
                }
             }

             // Progress does not validate the event

             Object rpub = publisher.get();
             boolean externalPublisher = publisher.isExternalResource();

             WorkArea wa = work.obtain();

             List<String> errors = new LinkedList<>();

             // determine the subscriptions
             // business logic can unsubscribe during this loop, so we have to use a copy of
             // the subscription list to avoid concurrent modification
             List<Subscription> evtSubs = wa.subscriptionsByEvent.get(event);
             ArrayList<Subscription> subCopy = evtSubs == null ? new ArrayList<>() : new ArrayList<>(evtSubs);
             subCopy.addAll(globalSubscriptions.getAllSubscriptions());

             Session currentSession = SessionManager.get().getSession();
             for (int j = 0; j < subCopy.size(); j++)
             {
                Subscription<?> sub = subCopy.get(j);
                // if the events match and the subscription is ANYWHERE or the publishers match,
                // then invoke the procedure
                if (sub.event.equals(event) &&
                   (sub.publisher == null || sub.publisher.equals(rpub)))
                {
                   // the parameters are resolved only when a subscription is found
                   Object[] param = parameters == null ? null : parameters.get();
                   if (param == null)
                   {
                      param = new Object[0];
                   }
                   if (currentSession == sub.session)
                   {
                      sub.publish((externalPublisher ? null : rpub), errors, modes, param);
                   }
                   else if (global)
                   {
                      Object[] params = new Object[param.length];
                      // create a copy of the params - as the OUTPUT/INPUT-OUTPUT params must not
                      // affect this session, and following are not supported:
                      // - extent parameters
                      // - field parameters
                      // - memptr
                      // - handle
                      // - buffers
                      // - blob
                      // - object
                      for (int i = 0; i < param.length; i++)
                      {
                         if (param[i] instanceof BaseDataType)
                         {
                            params[i] = ((BaseDataType) param[i]).duplicate();
                         }
                         else if (param[i] instanceof AbstractParameter ||
                                  param[i] instanceof object            ||
                                  param[i] instanceof blob              ||
                                  param[i] instanceof memptr            ||
                                  param[i] instanceof handle            ||
                                  param[i] instanceof WrappedResource   ||
                                  param[i] instanceof BufferReference)
                         {
                            throw new QuitConditionException("Parameter of type " +
                                                             param[i].getClass().toString() +
                                                             " is not supported!");
                         }
                         else if (param[i] != null && param[i].getClass().isArray())
                         {
                            throw new QuitConditionException("Array parameters are not supported!");
                         }
                         else
                         {
                            params[i] = param[i];
                         }
                      }

                      Runnable task = () -> sub.publishRemote(modes, params);
                      globalSubscriptions.post(new PublishTask(sub, task));
                   }
                }
             }

             if (!errors.isEmpty())
             {
                int[] nums = new int[errors.size()];
                Arrays.setAll(nums, operand -> 6480);
                ErrorManager.recordOrThrowError(nums, errors.toArray(new String[0]), false);
             }
          };

          // errors are not ignored if we are publishing a class event
          if (classEvent)
          {
             ptask.run();
          }
          else
          {
             silent(ptask);
          }
       }

       /**
        * Create a new subscription to the given event for a specified program. This program will
        * receive a call-back when the event is published.
        *
        * @param    subscriber
        *           The program to notify when the specified event is published. Must not be
        *           <code>null</code>.  The specified program must have a method that corresponds to
        *           the given Progress 4GL internal procedure name (<code>method</code>).
        * @param    event
        *           The name of the event which is being published.
        * @param    publisher
        *           The procedure handle representing the program that is the source of the event.
        *           If specified, the named event will only cause a notification if the event source
        *           matches this handle. <code>null</code> represents the <code>ANYWHERE</code> option
        *           which means that the events will not be filtered by source.
        * @param    method
        *           The Progress 4GL internal procedure name which will be translated into a method
        *           name and invoked on the <code>subscriber</code> instance. When <code>null</code>,
        *           it means that no method was specified for this call and the event name will be
        *           used as the method name.
        * @param    signature
        *           When specified, it represents the target to be called (part of the subscriber).
        *
        * @return   <code>true</code> if the subscription was registered.
        */
       static boolean subscribe(handle        subscriber,
                                character     event,
                                handle        publisher,
                                character     method,
                                InternalEntry signature)
       {
          return subscribe(false, subscriber, event, publisher, method, signature);
       }

       /**
        * Create a new subscription to the given event for a specified program. This program will
        * receive a call-back when the event is published.
        *
        * @param    global
        *           Flag indicating if the subscription is for global (cross-session) events.
        * @param    subscriber
        *           The program to notify when the specified event is published. Must not be
        *           <code>null</code>.  The specified program must have a method that corresponds to
        *           the given Progress 4GL internal procedure name (<code>method</code>).
        * @param    event
        *           The name of the event which is being published.
        * @param    publisher
        *           The procedure handle representing the program that is the source of the event.
        *           If specified, the named event will only cause a notification if the event source
        *           matches this handle. <code>null</code> represents the <code>ANYWHERE</code> option
        *           which means that the events will not be filtered by source.
        * @param    method
        *           The Progress 4GL internal procedure name which will be translated into a method
        *           name and invoked on the <code>subscriber</code> instance. When <code>null</code>,
        *           it means that no method was specified for this call and the event name will be
        *           used as the method name.
        * @param    signature
        *           When specified, it represents the target to be called (part of the subscriber).
        *
        * @return   <code>true</code> if the subscription was registered.
        */
       static boolean subscribe(boolean       global,
                                handle        subscriber,
                                character     event,
                                handle        publisher,
                                character     method,
                                InternalEntry signature)
       {
          // validate subscriber
          if (!subscriber.isExternalResource())
          {
             if (subscriber.isUnknown())
             {
                final String msg = "Could not evaluate subscriber handle  expression while trying to " +
                                   "execute SUBSCRIBE statement";
                ErrorManager.recordOrThrowError(6463, msg, false);
                return false;
             }
             if (!isProcedure(subscriber) || !subscriber._isValid())
             {
                final String msg = "Invalid or inappropriate subscriber handle value given to " +
                                   "SUBSCRIBE statement";
                ErrorManager.recordOrThrowError(6466, msg, false);
                return false;
             }
          }

          // validate publisher; if null, is OK, as it means ANYWHERE option
          if (publisher != null && !publisher.isExternalResource())
          {
             if (publisher.isUnknown())
             {
                final String msg = "Could not evaluate publisher handle  expression while trying " +
                                   "to execute SUBSCRIBE statement";
                ErrorManager.recordOrThrowError(6464, msg, false);
                return false;
             }
             if (!isProcedure(publisher) || !publisher._isValid())
             {
                final String msg = "Invalid or inappropriate publisher handle value given to " +
                                   "SUBSCRIBE statement";
                ErrorManager.recordOrThrowError(6467, msg, false);
                return false;
             }
          }
          // Progress does not validate event and method name

          Object rsub = subscriber.get();
          Object rpub = (publisher == null ? null : publisher.get());
          WorkArea wa = null;

          List<Subscription> subs;
          if (global)
          {
             subs = globalSubscriptions.getSubscriptions();
          }
          else
          {
             if (wa == null)
             {
                wa = work.obtain();
             }
             subs = wa.subscriptionsByEvent.computeIfAbsent(event, (k) -> new LinkedList<Subscription>());
          }

          if (subs == null)
          {
             return false;
          }

          // 4GL doesn't raise an error if a subscription already exists, but doesn't replace it
          // either
          boolean found = false;
          for (Subscription<?> sub : subs)
          {
             if (sub.readOnly)
             {
                // read-only subscriptions are not checked.
                continue;
             }

             // get an exact match
             if (sub.subscriber.equals(rsub) &&
                 event.equals(sub.event)     &&
                 ((rpub == null && sub.publisher == null) ||
                  (rpub != null && sub.publisher != null && rpub.equals(sub.publisher))))
             {
                found = true;
                break;
             }
          }

          if (found)
          {
             return false;
          }

          // create only if there isn't an existing one;
          // the subscription is added into a "global" list of subscriptions, the list
          // also holds the order when the subscriptions got subscribed so that they
          // can be fired in this order
          Subscription<?> sub = (rsub instanceof ExternalResource)
             ? new ExternalSubscription((ExternalResource) rsub, event, rpub)
             : new LegacySubscription(rsub, event, rpub, method);
          sub.signature = signature;

          if (global)
          {
             globalSubscriptions.subscribe(sub);
          }
          else
          {
             if (wa == null)
             {
                wa = work.obtain();
             }
             wa.subscriptions.add(sub);
             List<Subscription> evtSubs =
                wa.subscriptionsByEvent.computeIfAbsent(event, (k) -> new LinkedList<Subscription>());
             evtSubs.add(sub);
          }

          return true;
       }

       /**
        * Create a new subscription to the given event for any program. This program will receive a
        * call-back when the event is published.
        *
        * @param    global
        *           Flag indicating if the subscription is for global (cross-session) events.
        * @param    subscriber
        *           The program to notify when the specified event is published. Must not be
        *           <code>null</code>.  The specified program must have a method that corresponds to
        *           the given Progress 4GL internal procedure name (<code>method</code>).
        * @param    event
        *           The name of the event which is being published.
        * @param    method
        *           The Progress 4GL internal procedure name which will be translated into a method
        *           name and invoked on the <code>subscriber</code> instance. When <code>null</code>,
        *           it means that no method was specified for this call and the event name will be
        *           used as the method name.
        */
       static void subscribeAnywhere(boolean   global,
                                     handle    subscriber,
                                     character event,
                                     character method)
       {
          subscribe(global, subscriber, event, null, method, null);
       }

       /**
        * Create a new subscription to the given event for a specified external resource.
        * This resource will receive a notification when the event is published.
        *
        * @param    global
        *           Flag indicating if the subscription is for global (cross-session) events.
        * @param    subscriber
        *           The resource to notify when the specified event is published. If <code>null</code>,
        *           a {@link ExternalResource} will be built and its ID return.
        * @param    event
        *           The name of the event which is being published.
        * @param    publisher
        *           The handle representing the program or resource that is the source of the event.
        *           If specified, the named event will only cause a notification if the event source
        *           matches this handle. <code>null</code> represents the <code>ANYWHERE</code> option
        *           which means that the events will not be filtered by source.
        *
        * @return   The subscriber's ID.
        */
       static handle subscribeExternal(boolean global,
                                       handle  subscriber,
                                       String  event,
                                       handle  publisher)
       {
          if (subscriber == null)
          {
             subscriber = handle.buildExternalResource();
          }

          subscribe(global, subscriber, new character(event), publisher, null, null);

          return subscriber;
       }

       /**
        * Delete a subscription to the given event for the specified external resource. This method is
        * executed in a {@link ErrorManager#silent} bracket, as the <code>UNSUBSCRIBE</code>
        * statement is executed with a default NO-ERROR clause.
        *
        * @param    global
        *           Flag indicating if the search is done in the global (cross-session) event register.
        * @param    subscriber
        *           The resource whose subscription is to be deleted.  Must not be <code>null</code>.
        * @param    event
        *           The name of the event which is being published. If this is <code>null</code>, then
        *           subscriptions to all events will be deleted.
        * @param    publisher
        *           The procedure or resource handle representing the source of the event.
        *           If specified, the subscription will only be deleted if the event source matches
        *           this handle.  <code>null</code> represents the <code>ALL</code> option which means
        *           that the events will not be filtered by source. When <code>null</code>, it means
        *           that the publisher was not set.
        */
       static void unsubscribeExternal(boolean global,
                                       handle  subscriber,
                                       String  event,
                                       handle  publisher)
       {
          unsubscribe(global, subscriber, event == null ? null : new character(event), publisher, null);
       }

       /**
        * Notify all subscribers (to the given event) that the event has occurred.  This method is
        * executed in a {@link ErrorManager#silent} bracket, as the <code>PUBLISH</code>
        * statement is executed with a default NO-ERROR clause.
        * <p>
        * All processing is done with silent error mode implicitly enabled.
        * <p>
        * The subscriptions are fired in the order they were subscribed. Although 4GL
        * documentation specifically states, that "If a named event has multiple subscribers,
        * the order in which the AVM notifies subscribers is undefined.", the actual
        * 4GL implementation (OpenEdge 10.2B) does fire the events in the order the subscriptions
        * get subscribed.
        *
        * @param    event
        *           The name of the event which is being published.
        * @param    publisher
        *           The procedure or resource handle representing the program that is the source of
        *           the event. Must not be <code>null</code>.
        * @param    args
        *           The parameters passed to the subscriber. May be <code>null</code> to represent no
        *           parameters. When <code>null</code>, it means that no parameters were specified for
        *           this call.
        */
       static void publishExternal(String event, handle publisher, String[] args)
       {
          // convert the arguments from String to an Object element array
          Object[] param = new Object[args.length];
          System.arraycopy(args, 0, param, 0, args.length);
          publish(false, new character(event), publisher, null, () -> param);
       }

       /**
        * Remove all subscriptions registered by the given resource.  This may be a program handle
        * or a {@link ExternalResource}.
        *
        * @param    resource
        *           All subscriptions for this resource will be removed.
        */
       static void cleanupSubscriptions(Object resource)
       {
          WorkArea wa = work.obtain();
          Set<Subscription> removed = Collections.newSetFromMap(new IdentityHashMap<>());
          Iterator<Subscription> iter = wa.subscriptions.iterator();
          while (iter.hasNext())
          {
             Subscription sub = iter.next();

             if (sub.subscriber.equals(resource))
             {
                iter.remove();
                removed.add(sub);
             }
          }

          if (removed.isEmpty())
          {
             return;
          }

          Iterator<Map.Entry<character, LinkedList<Subscription>>> iter2 = wa.subscriptionsByEvent.entrySet().iterator();
          while (iter2.hasNext())
          {
             Map.Entry<character, LinkedList<Subscription>> entry = iter2.next();
             entry.getValue().removeAll(removed);
             if (entry.getValue().isEmpty())
             {
                iter2.remove();
             }
          }

          globalSubscriptions.removeAll(resource);
       }

       /**
        * Delete a subscription to the given event for the specified program. This method is executed
        * executed in a {@link ErrorManager#silent} bracket, as the <code>UNSUBSCRIBE</code>
        * statement is executed with a default NO-ERROR clause.
        *
        * @param    global
        *           Flag indicating if the search is done in the global (cross-session) event register.
        * @param    subscriber
        *           The program whose subscription is to be deleted.  Must not be <code>null</code>.
        * @param    event
        *           The name of the event which is being published. If this is <code>null</code>, then
        *           subscriptions to all events will be deleted.
        * @param    publisher
        *           The procedure handle representing the program that is the source of the event.
        *           If specified, the subscription will only be deleted if the event source matches
        *           this handle.  <code>null</code> represents the <code>ALL</code> option which means
        *           that the events will not be filtered by source. When <code>null</code>, it means
        *           that the publisher was not set.
        * @param    method
        *           The subscriber's method which needs to be removed.  Must be not-null for class
        *           events.
        */
       static void unsubscribe(boolean   global,
                               handle    subscriber,
                               character event,
                               handle    publisher,
                               character method)
       {
          // TODO: method name must be specified for CLASS EVENTS
          silent(() ->
          {
             if (!subscriber.isExternalResource())
             {
                // validate subscriber
                if (subscriber.isUnknown())
                {
                   final String msg = "Could not evaluate subscriber handle  expression while trying " +
                                      "to execute UNSUBSCRIBE statement";
                   ErrorManager.recordOrThrowError(6475, msg, false);
                   return;
                }
                if (!isProcedure(subscriber) || !subscriber._isValid())
                {
                   final String msg = "Invalid or inappropriate subscriber handle value given to " +
                                      "UNSUBSCRIBE statement";
                   ErrorManager.recordOrThrowError(6477, msg, false);
                   return;
                }
             }

             // validate publisher; if null, is OK, as it means ALL option
             if (publisher != null && !publisher.isExternalResource())
             {
                if (publisher.isUnknown())
                {
                   final String msg = "Could not evaluate publisher handle  expression while " +
                                      "trying to execute UNSUBSCRIBE statement";
                   ErrorManager.recordOrThrowError(6476, msg, false);
                   return;
                }
                if (!isProcedure(publisher) || !publisher._isValid())
                {
                   final String msg = "Invalid or inappropriate publisher handle value given to " +
                                      "UNSUBSCRIBE statement";
                   ErrorManager.recordOrThrowError(6478, msg, false);
                   return;
                }
             }

             // Progress does not validate the event

             Object rsub = subscriber.get();
             Object rpub = (publisher == null ? null : publisher.get());

             boolean found = false;

             WorkArea wa = work.obtain();
             List<Subscription> subs;
             if (global)
             {
                subs = globalSubscriptions.getSubscriptions();
             }
             else
             {
                subs = event == null ? work.obtain().subscriptions : wa.subscriptionsByEvent.get(event);
                if (subs == null)
                {
                   subs = Collections.emptyList();
                }
             }

             Set<Subscription> removed = Collections.newSetFromMap(new IdentityHashMap<>());
             Iterator<Subscription> itr = subs.iterator();
             while (itr.hasNext())
             {
                Subscription sub = itr.next();

                // a special case: if a publisher is specified and ALL clause is used, than all
                // ANYWERE subscriptions are marked read-only, for the event.
                if (rpub != null && event == null && sub.publisher == null)
                {
                   sub.readOnly = true;
                }

                if (sub.readOnly)
                {
                   // read-only subscriptions can't be removed
                   continue;
                }

                // if removes all subscriptions considering:
                // - if the passed event is null, then ALL clause is in effect, else event names must
                // match
                // - the publisher is not matched as stated in the documentation. the correct approach
                // is that if the publisher is missing, then ANY publisher is matched
                if (sub.subscriber.equals(rsub)                &&
                    (event == null || event.equals(sub.event)) &&
                    ((rpub == null || rpub.equals(sub.publisher))))
                {
                   // TODO: in case of class events, the METHOD must match, too

                   if (global)
                   {
                      globalSubscriptions.removeAll(sub);
                   }
                   else
                   {
                      // remove this subscription
                      itr.remove();
                      removed.add(sub);
                   }

                   found = true;
                }
             }

             if (!removed.isEmpty())
             {
                if (event == null)
                {
                   Iterator<Map.Entry<character, LinkedList<Subscription>>> iter =
                      wa.subscriptionsByEvent.entrySet().iterator();
                   while (iter.hasNext())
                   {
                      Map.Entry<character, LinkedList<Subscription>> entry = iter.next();
                      entry.getValue().removeAll(removed);
                      if (entry.getValue().isEmpty())
                      {
                         iter.remove();
                      }
                   }
                }
                else
                {
                   if (subs.isEmpty())
                   {
                      wa.subscriptionsByEvent.remove(event);
                   }

                   wa.subscriptions.removeAll(removed);
                }
             }

             // if a specific event is used and no subscriptions found, then error
             if (!found && event != null)
             {
                final String msg = "UNSUBSCRIBE failed.  No subscriptions for %s";
                String sevt = (event.isUnknown() ? "" : event.toStringMessage());
                ErrorManager.recordOrThrowError(6479, String.format(msg, sevt), false);
                return;
             }
          });
       }

       /**
        * Delete a subscription to ALL events for the specified program. This method is
        * executed in a {@link ErrorManager#silent} bracket, as the <code>UNSUBSCRIBE</code>
        * statement is executed with a default NO-ERROR clause.
        *
        * @param    global
        *           Flag indicating if the subscription is for global (cross-session) events.
        * @param    subscriber
        *           The program whose subscription is to be deleted.  Must not be <code>null</code>.
        * @param    publisher
        *           The procedure handle representing the program that is the source of the event.
        *           If specified, the subscription will only be deleted if the event source matches
        *           this handle.  <code>null</code> represents the <code>ALL</code> option which means
        *           that the events will not be filtered by source.  When <code>null</code>, it means
        *           that the publisher was not set.
        */
       static void unsubscribeAll(boolean global, handle subscriber, handle publisher)
       {
          unsubscribe(global, subscriber, null, publisher, null);
       }

       /**
        * Ensure all the context-local data has initialized (to be able to listen
        * for scope notifications).
        */
       public static void load()
       {
          // needed to ensure the variable is loaded, so it listens for scope notifications
          work.obtain();
       }

       /**
        * Remove the given handle from the list of persistent procedures. Used by
        * {@link ControlFlowOps#invokePersistent(character, Object...)} and
        * {@link ControlFlowOps#invokePersistentSet(String, handle, Object...)} in
        * cases when errors were encountered while invoking the procedure.
        *
        * @param   h
        *          The procedure handle to be added to the list of persistent
        *          procedures.
        */
       static void removePersistentProcedure(handle h)
       {
          WorkArea wa = work.obtain();
          removeProcedureImpl(wa, h, wa.persistentProcedures);
          locateProcedure(wa, h.get()).persistent = false;
          wa.removeStaleProcedure(h.get());
       }

       /**
        * Implementation for the PROGRAM-NAME functions. Resolves the legacy top level block (external
        * program, function, internal procedure or trigger), at the given depth. The index starts with
        * 0 for the currently executing top-level block.
        *
        * @param    idx
        *           The stack entry index.
        *
        * @return   The name of the stack entry or {@code null} if outside of stack.
        */
       static String getStackEntry(int idx)
       {
          return work.obtain().getStackEntry(idx);
       }

       /**
        * Get the legacy stack trace
        *
        * @return the legacy stack trace
        */

       public static List<String> getStackTrace()
       {
          return getStackTrace(work.obtain());
       }

       /**
        * Get the legacy stack trace
        *
        * @param    wa
        *           The context-local state.
        *
        * @return   the legacy stack trace
        */

       public static List<String> getStackTrace(WorkArea wa)
       {
          List<String> stack = new ArrayList<>();

          Iterator<ProcedureData> procItr = wa.thisProceduresData.iterator();
          Iterator<String> ieItr = wa.internalEntryStack.iterator();

          while (procItr.hasNext())
          {
             Object referent = procItr.next().getReferent();
             String iename = ieItr.next();

             if (PERSISTENT_TRIGGER_PROCEDURE.equals(iename) || THIS_PROCEDURE_OVERRIDE.equals(iename))
             {
                continue;
             }

             String name = wa.relativeNames.get(referent);
             stack.add(EXTERNAL_PROGRAM.equals(iename) ? name : (iename + " " + name));
          }
          return stack;
       }

       /**
        * Build a trace line including both Java and legacy names.
        *
        * @param className
        *        Java class name
        * @param methodName
        *        Java method name
        * @param fileName
        *        Java source file name
        * @param lineNumber
        *        Java source line number
        *
        * @return the trace line.
        */
       public static String buildTraceLine(
                final String className,
                final String methodName,
                final String fileName,
                final int lineNumber)
       {
          boolean externalProgram = methodName.equals("execute");
          boolean ooInitializer = methodName.startsWith("__") && methodName.endsWith("_execute__");

          Class<?> cls = null;
          try
          {
             cls = Class.forName(className);
          }
          catch (Throwable t)
          {
             // Ignore
          }

          final String pname = SourceNameMapper.getLegacySourceName(className);

          String iename;
          if (cls != null && _BaseObject_.class.isAssignableFrom(cls))
          {
             // legacy OO class
             iename = SourceNameMapper.findLegacyEntryName(cls, methodName);
          }
          else
          {
             // external program
             iename = null;
             if (pname != null)
             {
                iename = SourceNameMapper.convertJavaMethod(className, methodName, false);
                if (iename == null)
                {
                   iename = SourceNameMapper.convertJavaMethod(className, methodName, true);
                }
             }
          }

          String ptrace = null;

          if (pname != null)
          {
             if (externalProgram || ooInitializer)
             {
                ptrace = pname;
             }
             else if (iename != null)
             {
                ptrace = pname + " " + iename;
             }
          }

          return className + "." + methodName
                 + (ptrace == null ? "" : "[" + ptrace + "]")
                 + " at line " + lineNumber
                 + " (" + fileName + ")";
       }

       /**
        * Get the absolute name of the external program associated with the given java instance.
        * <p>
        * This is used internally by P2J, as we need absolute names when resolving a program's
        * signature, events, etc.
        * <p>
        * For the name associated with the procedure's NAME and FILE-NAME attributes, and returned by
        * the PROGRAM-NAME function or used in 4GL error messages, see {@link #getRelativeName}, which
        * returns the real name used when the external procedure was instantiated first.
        *
        * @param    referent
        *           The referent for which the name is needed.
        *
        * @return   The absolute name.
        */
       static String getAbsoluteName(Object referent)
       {
          String jname = null;

          if (referent.getClass() == Object.class)
          {
             jname = ObjectOps.getStaticClass(referent).getName();
          }
          else
          {
             jname = referent.getClass().getName();
          }

          String pname = SourceNameMapper.getLegacySourceName(jname);
          return pname;
       }

       /**
        * Check if the given referent has been (or is still) registered with the
        * {@link ProcedureManager} in the {@link WorkArea#pMap procedure} map.
        *
        * @param     referent
        *            The referent to be checked if is still loaded.
        *
        * @return    <code>true</code> if the referent was found.
        */
       public static boolean hasReferent(Object referent)
       {
          return hasReferent(work.obtain(), referent);
       }

       /**
        * Delete the given procedure handle (i.e. execute the DELETE OBJECT or DELETE PROCEDURE
        * handle).
        *
        * @param    referent
        *           The persistent procedure instance to be deleted.
        * @param    letStaleProcedure
        *           Flag to indicate if the procedure should go in an stale state.
        *
        * @return   <code>true</code> if this procedure handle can be deleted.
        */
       static boolean delete(Object referent, boolean letStaleProcedure)
       {
          return delete(null, referent, letStaleProcedure);
       }

       /**
        * Delete the given procedure handle (i.e. execute the DELETE OBJECT or DELETE PROCEDURE
        * handle).
        *
        * @param    wa
        *           The context-local state.
        * @param    referent
        *           The persistent procedure instance to be deleted.
        * @param    letStaleProcedure
        *           Flag to indicate if the procedure should go in an stale state.
        *
        * @return   <code>true</code> if this procedure handle can be deleted.
        */
       static boolean delete(WorkArea wa, Object referent, boolean letStaleProcedure)
       {
          if (wa == null)
          {
             wa = work.obtain();
          }

          boolean result = true;
          if (referent instanceof _BaseObject_)
          {
             ObjectResource res = ObjectOps.asResource((_BaseObject_) referent);
             if (!res.isPendingDelete())
             {
                result = ObjectOps.delete((_BaseObject_) referent);
             }
          }

          // the referent may already have been deleted by ObjectOps.delete
          result = result && (!wa.pMap.containsKey(referent) || delete(wa, referent));

          if (!result || ObjectOps.isLegacyObject(referent) || !wa.hasPersistentProcStack(referent))
          {
             // legacy OO does not have the 'stale procedure' behavior.
             return result;
          }

          if (letStaleProcedure)
          {
             StaleProcedureManager.addStaleReferent(referent);
          }
          else
          {
             StaleProcedureManager.notifyResourceDelete(referent);
          }

          return true;
       }

       /**
        * Check if this referent can be deleted or not.
        * <p>
        * This checks if the instance is on stack, and if the run is SINGLE-RUN or SINGLETON.
        *
        * @param    wa
        *           The {@link WorkArea} instance.
        * @param    referent
        *           The instance to check.
        *
        * @return   <code>true</code> if the delete must be postponed.
        */
       static boolean isDelayedDelete(WorkArea wa, Object referent)
       {
          ProcedureData pdata = locateProcedure(wa, referent);

          // do not postpone the delete when the referent is a TARGET-PROCEDURE or
          // a SOURCE-PROCEDURE
          if (pdata != null && pdata.persistent && pdata.thisCount > 0)
          {
             if (pdata.singleRun)
             {
                return true;
             }

             if (pdata.singleton)
             {
                // raise an ERROR condition
                wa.tm.triggerErrorInCaller();
                return true;
             }

             // if this is a persistent system handle, the actual delete is postponed until we
             // are finished.
             wa.deleteOnExit.put(referent, true);

             if (LOG.isLoggable(Level.FINE))
             {
                LOG.log(Level.FINE, "Postponing deletion of procedure " + getAbsoluteName(referent));
             }

             return true;
          }

          return false;
       }

       /**
        * Delete the given procedure handle (i.e. execute the DELETE OBJECT or DELETE PROCEDURE
        * handle).
        *
        * @param    wa
        *           The context-local state in {@link WorkArea}.
        * @param    referent
        *           The persistent procedure instance to be deleted.
        *
        * @return   <code>true</code> if this procedure handle can be deleted.
        */
       private static boolean delete(WorkArea wa, Object referent)
       {
          // handle is always valid

          if (isDelayedDelete(wa, referent))
          {
             return false;
          }

          ProcedureData pdata = locateProcedure(wa, referent);

          if (TransactionImpl.isTxInitProcedure(referent))
          {
             if (TransactionImpl.isOpenByTxInitProc())
             {
                TransactionImpl.requestFinished(false, false);
             }

             // if this is the TRANS-INIT-PROCEDURE, then remove the reference and clear the pending
             // flags
             TransactionImpl.setTxInitProcedure(null);
          }

          boolean invalidateCallSiteCache = false;
          wa.persistentProcedures.remove(referent);
          if (pdata.isSessionSuperProcedure)
          {
             invalidateCallSiteCache = true;
             wa.sessionSuperProcs.remove(referent);
          }
          wa.singletonMap.remove(referent.getClass());

          if (!pdata.superProcedures.isEmpty())
          {
             for (ProcedureData pp : pdata.superProcedures.values())
             {
                // clean the super-procedure list from the reverse-tracking of the referent.
                boolean removed = pp.addedToSuperProcedures.remove(referent);
                invalidateCallSiteCache = invalidateCallSiteCache || removed;
             }
          }

          if (!pdata.addedToSuperProcedures.isEmpty())
          {
             for (Object ref : pdata.addedToSuperProcedures)
             {
                invalidateCallSiteCache = true;

                ProcedureData pp = locateProcedure(wa, ref);
                if (pp != null)
                {
                   // the procedure to which this was added may have already been deleted.
                   pp.superProcedures.remove(referent);
                }
             }
          }

          // force the cleanup here (do not use cleanup()), we don't want to cause any inconsistency
          // here, when this method proceeds with clean up do the cleanup completely
          wa.forceCleanup(referent);
          Object prevReferent = wa.processedProcedure;
          List<String> errors = new ArrayList<>();
          List<Integer> nums = new ArrayList<>();

          try
          {
             wa.processedProcedure = referent;

             BiConsumer<String, Integer> processor = (msg, num) ->
             {
                errors.add(msg);
                nums.add(num);
             };

             // cleanup after the procedure
             if (!pdata.finalizables.isEmpty())
             {
                for (Finalizable target : pdata.finalizables)
                {
                   wa.errHelper.nestedSilent(target::deleted, processor);
                }
             }

             if (!pdata.scopeables.isEmpty())
             {
                for (Scopeable target : pdata.scopeables)
                {
                   wa.errHelper.nestedSilent(target::scopeDeleted, processor);
                }
             }

             // as the procedure no longer exists, delete the static resources
             // this can have multiple scopes, one for each class in the OE hierarchy.
             while (!pdata.staticResources.isEmpty())
             {
                deleteResources(wa, pdata.staticResources.pop());
             }
          }
          finally
          {
             wa.processedProcedure = prevReferent;
          }

          handle unknown = new handle();

          // remove it from the instantiating procedure
          for (HandleResource res : pdata.createdResources.keySet())
          {
             res.setInstantiatingProcedure(unknown);
          }

          // delete the widget pools for this external program
          wa.wp.deleteWidgetPools(referent);

          // get rid of references...
          pdata.createdResources.clear();
          pdata.scopeables.clear();
          pdata.finalizables.clear();

          // remove any triggers created in this-procedure
          LogicalTerminal.cleanupTriggers(referent);

          handle.removeResource(pdata.phandle.getResource());

          // clear all undoable variables created by this procedure (they might have been tracked
          // in parent tx blocks).
          wa.tm.untrackUndoables(referent);

          if (invalidateCallSiteCache)
          {
             ControlFlowOps.invalidateCallSiteCache();
          }
          else
          {
             ControlFlowOps.invalidateCallSiteCache(referent);
          }

          if (!errors.isEmpty())
          {
             // force an ERROR condition to be raised and displayed - it will ignore NO-ERROR mode
             ErrorManager.displayAndRaiseError(errors);
          }

          return true;
       }

       /**
        * Check if the given handle is a proxy procedure.
        *
        * @param    h
        *           The handle to check.
        *
        * @return   <code>true</code> if the handle is a proxy procedure.
        */
       static boolean isProxy(handle h)
       {
          return h != null && (h.getResource() instanceof ProxyProcedureWrapper) && h._isValid();
       }

       /**
        * Check if the given referent is for a system handle (i.e. for one of the
        * THIS-PROCEDURE, SOURCE-PROCEDURE or TARGET-PROCEDURE handles).
        *
        * @param    referent
        *           An instance of an converted external procedure class.
        *
        * @return   See above.
        */
       static boolean systemHandle(Object referent)
       {
          WorkArea wa = work.obtain();
          ProcedureData pd = wa.pMap.get(referent);

          return pd != null && (pd.thisCount > 0 || pd.sourceCount > 0 || pd.targetCount > 0);
       }

       /**
        * Find the root enclosing instance of the specified inner class instance.
        * It gets computed using the <code>this$</code> fields inserted by the
        * compiler; it returns the instance refered by the <code>this$0</code>
        * fields, when reached.
        *
        * @param    instance
        *           The inner class instance.
        *
        * @return   The root instance.
        */
       static Object findRootEnclosingInstance(Object instance)
       {
          Field[] fields = instance.getClass().getDeclaredFields();
          for (Field f : fields)
          {
             String fname = f.getName();
             if (fname.startsWith("this$"))
             {
                try
                {
                   f.setAccessible(true);
                   Object outer = f.get(instance);

                   return fname.equals("this$0") ? outer : findRootEnclosingInstance(outer);
                }
                catch (Exception e)
                {
                   LOG.severe("", e);
                   return null;
                }
             }
          }

          return null;
       }

       /**
        * Set the name of the 4GL program currently being instantiated, into the
        * {@link WorkArea#initiatingExtProg} field.
        *
        * @param    name
        *           The 4GL external program name.
        */
       public static void setInstantingExternalProgram(String name)
       {
          work.obtain().setInstantingExternalProgram(name);
       }

       /**
        * Walk the {@link WorkArea#thisProceduresData THIS-PROCEDURE} stack and apply the search function;
        * return the first value given by the search function, once it returns a non-null value.
        *
        * @param    search
        *           The search function.
        *
        * @return   The first non-null value returned by the search function.
        */
       static Object searchInStack(Function<Object, Object> search)
       {
          return work.obtain().searchInStack(search);
       }

       /**
        * Get the THIS-PROCEDURE system handle.
        *
        * @param    wa
        *           Context-local work area.
        *
        * @return   See above.
        */
       private static handle thisProcedure(WorkArea wa)
       {
          ProcedureData pdata = wa.thisProceduresData.peek();

          return (pdata != null) ? new handle(pdata.phandle) : new handle();
       }

       /**
        * Get the THIS-PROCEDURE referent.
        *
        * @param    wa
        *           Context-local work area.
        *
        * @return   See {@link #thisProcedure()}.
        */
       private static Object _thisProcedure(WorkArea wa)
       {
          ProcedureData pd = wa.thisProceduresData.peek();

          return pd == null ? null : pd.getReferent();
       }

       /**
        * Get the TARGET-PROCEDURE system handle.
        *
        * @param    wa
        *           Context-local work area.
        *
        * @return   See above.
        */
       private static handle targetProcedure(WorkArea wa)
       {
          // Do not use locateProcedure() here, the system handle may have been deleted
          // with DELETE PROCEDURE. Note that when a procedure is deleted it can be
          // referenced and queried through the system procedure handles.
          return new handle(wa.targetProceduresData.peek().phandle);
       }

       /**
        * Check if the given referent is for a persistent procedure. This implements the PERSISTENT
        * attribute for a procedure handle.
        *
        * @param    wa
        *           Context-local work area.
        * @param    referent
        *           An instance of a converted external procedure class.
        *
        * @return   <code>true</code> if the referent is a persistent procedure.
        */
       private static boolean _isPersistent(WorkArea wa, Object referent)
       {
          ProcedureData pdata = locateProcedure(wa, referent);
          return pdata != null && pdata.persistent;
       }

       /**
        * Check if the given referent is for a single-run procedure. This implements the SINGLE-RUN
        * attribute for a procedure handle.
        *
        * @param    wa
        *           Context-local work area.
        * @param    referent
        *           An instance of a converted external procedure class.
        *
        * @return   <code>true</code> if the referent is a single-run procedure.
        */
       private static boolean _isSingleRun(WorkArea wa, Object referent)
       {
          ProcedureData pdata = locateProcedure(wa, referent);
          return pdata != null && pdata.singleRun;
       }

       /**
        * Check if the given referent is for a singleton procedure. This implements the SINGLETON
        * attribute for a procedure handle.
        *
        * @param    wa
        *           Context-local work area.
        * @param    referent
        *           An instance of a converted external procedure class.
        *
        * @return   <code>true</code> if the referent is a singleton procedure.
        */
       private static boolean _isSingleton(WorkArea wa, Object referent)
       {
          ProcedureData pdata = locateProcedure(wa, referent);
          return pdata != null && pdata.singleton;
       }

       /**
        * Push an internal entry to the stack.  The passed internal entry name is resolved and is
        * pushed as it was defined, not as it is being called.
        * <p>
        * Use {@link #EXTERNAL_PROGRAM} for the name of the root external procedure block and
        * {@link #TRIGGER_PROCEDURE} for the root trigger block.
        *
        * @param    wa
        *           Context-local work area.
        * @param    instance
        *           The external program instance.
        * @param    iename
        *           The legacy internal-entry name.
        * @param    function
        *           Flag indicating if the internal entry is a user-defined function.
        * @param    legacyName
        *           The already known legacy name for the internal entry.
        */
       private static void pushInternalEntry(WorkArea wa,
                                             Object   instance,
                                             String   iename,
                                             boolean  function,
                                             String   legacyName)
       {
          if (!(iename.equals(EXTERNAL_PROGRAM) ||
                iename.equals(TRIGGER_PROCEDURE) ||
                iename.equals(PERSISTENT_TRIGGER_PROCEDURE) ||
                iename.equals(THIS_PROCEDURE_OVERRIDE)))
          {
             // the internal entry names are reported as they are defined, not as they are called.
             // thus, we need to determine the definition and get it from there

             if (legacyName == null)
             {
                // if not already resolved, resolve it now
                String pname = getAbsoluteName(instance);
                InternalEntry ie = SourceNameMapper.getInternalEntry(pname, iename, function);
                iename = (ie == null ? iename : ie.pname);
             }
             else
             {
                iename = legacyName;
             }
          }

          wa.internalEntryStack.push(iename);
       }

       /**
        * Add the <code>h</code> handle as a persistent procedure.
        *
        * @param   wa
        *          Context-local work area.
        * @param   h
        *          The procedure handle to be added to the list of persistent
        *          procedures.
        * @param   name
        *          The name of this procedure, used at the RUN statement.
        * @param   persistent
        *          Flag indicating if this is a persistent procedure.
        * @param   singleton
        *          Flag indicating if this is a singleton procedure.
        * @param   singleRun
        *          Flag indicating if this is a single-run procedure.
        * @param   legacyClass
        *          Set to {@code true} if the method is called as a result of
        *          legacy class initialization, {@code false} otherwise.
        */
       private static void addProcedure(WorkArea wa,
                                        handle   h,
                                        String   name,
                                        boolean  persistent,
                                        boolean  singleton,
                                        boolean  singleRun,
                                        boolean  legacyClass)
       {
          Object referent = h.get();
          ProcedureData pp = new ProcedureData(h, null, name);
          pp.singleton = singleton;
          pp.singleRun = singleRun;
          pp.persistent = persistent;

          ProcedureData prev = wa.pMap.put(referent, pp);
          if (prev == null && !legacyClass)
          {
             TranslationManager tm = TranslationManager.getInstance();
             tm.applyLanguage(((TransparentWrapper) h.getResource()).get().getClass());
          }

          if (persistent)
          {
             wa.persistentProcedures.add(referent);
          }
          if (singleton)
          {
             wa.singletonMap.put(referent.getClass(), referent);
          }
          wa.relativeNames.put(referent, name);

          if (wa.defaultDbs != null)
          {
             String rpkg = null;
             if (referent.getClass() == Object.class)
             {
                // a static OO class placeholder
                Class<?> cls = ObjectOps.getStaticClass(referent);
                rpkg = cls.getPackage().getName();
             }
             else
             {
                rpkg = referent.getClass().getPackage().getName();
             }
             rpkg = rpkg + ".";

             for (String pkg : wa.defaultDbs.keySet())
             {
                if (rpkg.startsWith(pkg))
                {
                   if (pp.connectedDbs == null)
                   {
                      pp.connectedDbs = new HashSet<>();
                   }

                   pp.connectedDbs.addAll(wa.defaultDbs.get(pkg));
                }
             }
          }
       }

       /**
        * Implementation for the SUPER-PROCEDURES attribute.
        *
        * @param    superProcs
        *           A list of {@link ProcedureData} objects, in the order in
        *           which they were added.
        *
        * @return   The string representation of each super-procedure, separated
        *           by a comma. The last added super-procedure must be first in
        *           this list, the second-to-last must be the second in the list,
        *           etc.
        */
       private static character superProceduresImpl(Collection<ProcedureData> superProcs)
       {
          character sp = new character("");
          boolean first = true;
          ArrayDeque<ProcedureData> queue = new ArrayDeque<>();
          queue.addAll(superProcs);
          // need to emit them in a LIFO order
          while (!queue.isEmpty())
          {
             ProcedureData pp = queue.removeLast();
             long hashCode = handle.resourceId(pp.phandle.getResource());
             sp.assign(TextOps.concat(sp, (!first ? "," : ""), hashCode));
             first = false;
          }

          return sp;
       }

       /**
        * The implementation of the ADD-SUPER-PROCEDURE method. When the
        * <code>referent</code> is null, this call is for the SESSION handle.
        *
        * @param    wa
        *           Context-local work area.
        * @param    h
        *           A handle to be added as super-procedure.
        * @param    mode
        *           The {@link SearchMode} for the passed handle.
        * @param    referent
        *           When <code>null</code> add to the SESSION:SUPER-PROCEDURES
        *           list. Else, add to the procedure associated with this
        *           referent.
        *
        * @return   <code>true</code> if <code>h</code> is a valid procedure
        *           handle.
        */
       private static logical addSuperProcedureImpl(WorkArea wa, handle h, SearchMode mode, Object referent)
       {
          if (!h._isValid() || !isProcedure(h))
          {
             return new logical(false);
          }

          ControlFlowOps.invalidateCallSiteCache();

          Map<Object, ProcedureData> superProcs = wa.getSuperProcedures(referent, false);
          ProcedureData pData = locateProcedure(wa, h.get());
          if (referent == null)
          {
             pData.isSessionSuperProcedure = false;
          }
          else
          {
             pData.addedToSuperProcedures.remove(referent);
          }

          // the super-procedure to be added to the chain will always exist (either as the current
          // this-procedure or as a persistent procedure). the remove call is needed just to eliminate
          // it from the referent's super procedure chain, if was previously added
          removeProcedureImpl(wa, h, superProcs.keySet());

          if (!(mode == null || pData.searchMode == null || pData.searchMode.equals(mode)))
          {
             // if the mode is changed, show warning
             int errNum = 8914;
             String msg = "**Changing proc-search for procedure %s from  '%s' to '%s'";

             String progName = getRelativeName(wa, referent);
             String oldMode  = pData.searchMode.toString();
             String newMode  = mode.toString();

             msg = String.format(msg, progName, oldMode, newMode);

             // this is just a warning, the mode will be changed
             ErrorManager.recordOrShowError(errNum, msg, false, false, false);
          }

          pData.searchMode = mode;
          superProcs.put(h.get(), pData);
          if (referent == null)
          {
             pData.isSessionSuperProcedure = true;
          }
          else
          {
             if (pData.addedToSuperProcedures == Collections.EMPTY_SET)
             {
                pData.addedToSuperProcedures = Collections.newSetFromMap(new IdentityHashMap<>());
             }
             pData.addedToSuperProcedures.add(referent);
          }

          return logical.TRUE;
       }

       /**
        * Removes the given procedure handle from the given procedure instances list.
        *
        * @param    wa
        *           Context-local work area.
        * @param    h
        *           The procedure handle to be removed.
        * @param    ppList
        *           The collection from which the procedure must be removed.
        */
       private static void removeProcedureImpl(WorkArea wa, handle h, Collection<Object> ppList)
       {
          // if the handle is null or the list is empty, there is nothing to do
          if (h == null || ppList.isEmpty())
          {
             return;
          }

          Object hreferent = h.get();

          Iterator<Object> iter = ppList.iterator();
          while (iter.hasNext())
          {
             Object referent = iter.next();
             if (hreferent == referent)
             {
                iter.remove();
             }
          }

          wa.cleanup(hreferent);
       }

       /**
        * Get the {@link SearchMode} constant associated with the given mode, used
        * by ADD-SUPER-PROCEDURE calls. If the mode is not one of the SEARCH-SELF
        * or SEARCH-TARGET values, or an {@link IllegalArgumentException} will be
        * thrown.
        * <p>
        * The given <code>mode</code> is not compatible with the mode returned by
        * {@link SearchMode#toString()}.
        *
        * @param    mode
        *           The string representation of the search-mode.
        *
        * @return   For SEARCH-TARGET value, {@link SearchMode#SEARCH_TARGET}
        *           will be returned. For SEARCH-SELF value,
        *           {@link SearchMode#SEARCH_SELF} will be returned.
        *
        * @throws   IllegalArgumentException
        *           If the mode is not of a known value.
        */
       private static SearchMode getSearchMode(int mode)
       {
          if (mode == SEARCH_SELF)
             return SearchMode.SEARCH_SELF;

          if (mode == SEARCH_TARGET)
             return SearchMode.SEARCH_TARGET;

          // if not a known mode, default to "search mode not specified"
          return null;
       }

       /**
        * Get the relative name used to create the given external program instance.
        *
        * @param    instance
        *           The external program instance.
        *
        * @return   See above.
        */
       private static String getRelativeName(WorkArea wa, Object instance)
       {
          return wa.relativeNames.get(instance);
       }

       /**
        * Check if the given referent has been (or is still) registered with the
        * {@link ProcedureManager} in the {@link WorkArea#pMap procedure} map.
        *
        * @param     wa
        *            Context-local work area.
        * @param     referent
        *            The referent to be checked if is still loaded.
        *
        * @return    <code>true</code> if the referent was found.
        */
       private static boolean hasReferent(WorkArea wa, Object referent)
       {
          return wa.pMap.containsKey(referent);
       }

       /**
        * Check if the specified referent is stale or not.
        *
        * @param    wa
        *           The work area for which the query is done.
        * @param    referent
        *           The referent which is checked for staleness
        *
        * @return   {@code true} if the given referent is stale.
        */
       private static boolean hasStaleReferent(WorkArea wa, Object referent)
       {
          return wa.staleProcedures.contains(referent);
       }

       /**
        * Check whether the specified referent (procedure) has been executed or not.
        *
        * @param    wa
        *           The work area for which the query is done.
        * @param    referent
        *           The referent which is checked for having been executed. Must not be {@code null} and must
        *           represent a valid procedure referent.
        *
        * @return   {@code true} if the procedure has been executed or was not found (i.e., is assumed to have
        *           executed and been deleted/discarded); else {@code false}.
        *
        * @throws   IllegalStateException
        *           if the procedure data for the given referent cannot be found.
        */
       public static boolean isExecuted(WorkArea wa, Object referent)
       {
          ProcedureData pdata = locateProcedure(wa, referent);

          if (pdata == null)
          {
             throw new IllegalStateException("Unable to find procedure for referent: " + referent);
          }

          return pdata.executed;
       }

       /**
        * Locate the {@link ProcedureData} instance associated with the given external program
        * instance.
        * <p>
        * This will find any persistent procedure or any procedure active on the stack.
        *
        * @param    referent
        *           The external program instance.
        *
        * @return   See above.
        */
       private static ProcedureData locateProcedure(Object referent)
       {
          return locateProcedure(work.obtain(), referent);
       }

       /**
        * Locate the {@link ProcedureData} instance associated with the given external program
        * instance.
        * <p>
        * This will find any persistent procedure or any procedure active on the stack.
        *
        * @param    wa
        *           Context-local work area.
        * @param    referent
        *           The external program instance.
        *
        * @return   See above.
        */
       private static ProcedureData locateProcedure(WorkArea wa, Object referent)
       {
          return wa.pMap.get(referent);
       }

       /**
        * Delete the given resources, as long as they are still valid.
        *
        * @param    wa
        *           The {@link WorkArea} instance.
        * @param    resources
        *           The resources to be deleted.
        */
       private static void deleteResources(WorkArea wa, Set<WrappedResource> resources)
       {
          if (resources.isEmpty())
          {
             return;
          }

          for (WrappedResource target : resources)
          {
             if (target.valid())
             {
                wa.pendingResourceDelete = target;
                try
                {
                   ((Deletable) target).delete();
                }
                finally
                {
                   wa.pendingResourceDelete = null;
                }
             }
          }
       }

       /**
        * Remove elements from the end of each queue until the specified referent is found.
        *
        * @param   queues
        *          The queues which need to be prepared.
        * @param   ref
        *          The referent on which removing will stop.  This will be also removed, if found.
        */
       private static void prepareSearches(Collection<?>[] queues, Object ref)
       {
          l1: for (Collection<?> queue_i : queues)
          {
             Deque<Object> queue = new ArrayDeque<>(queue_i);
             while (!queue.isEmpty())
             {
                Object referent = queue.removeLast();

                queue_i.remove(referent);

                if (referent == ref)
                {
                   break l1;
                }
             }
          }
       }

       /**
        * Helper to expose APIs which have direct access to the context-local state.
        */
       public static class ProcedureHelper
       {
          /** Cache and expose the {@link WidgetPool.WidgetPoolHelper}. */
          final WidgetPool.WidgetPoolHelper wp;

          /** Cache and expose the {@link handle.Helper}. */
          final handle.Helper h;

          /** Cache and expose the {@link ObjectOps.ObjectHelper}. */
          final ObjectOps.ObjectHelper oh;

          /** The {@link WorkArea} instance. */
          private final WorkArea wa;

          /**
           * Create a new instance and associate the given WorkArea instance.
           *
           * @param    wa
           *           The {@link WorkArea} instance.
           */
          private ProcedureHelper(WorkArea wa)
          {
             this.wa = wa;
             this.wp = wa.wp;
             this.h = wa.h;
             this.oh = wa.oh;
          }

          /**
           * Allow static resources to be registered to {@link WorkArea#pendingResources}.
           */
          public void enablePendingResources()
          {
             wa.registerPendingResources = true;
          }

          /**
           * Disable static resources to be registered to {@link WorkArea#pendingResources}.
           */
          public void disablePendingResources()
          {
             wa.registerPendingResources = false;
          }

          /**
           * Process the given resource by either setting its INSTANTIATING-PROCEDURE attribute
           * immediately (in case of non-static resources) or add the static resource to the
           * {@link WorkArea#pendingResources} set, to be processed when the next scope is entered.
           *
           * @param    res
           *           The resource to be processed.
           * @param    dynamic
           *           Flag indicating if this is a dynamic or static resource.
           */
          public void processResource(WrappedResource res, boolean dynamic)
          {
             ProcedureManager.processResource(wa, res, dynamic);
          }

          /**
           * Check if this referent can be deleted or not.
           * <p>
           * This checks if the instance is on stack, and if the run is SINGLE-RUN or SINGLETON.
           *
           * @param    referent
           *           The instance to check.
           *
           * @return   <code>true</code> if the delete must be postponed.
           */
          public boolean isDelayedDelete(Object referent)
          {
             return ProcedureManager.isDelayedDelete(wa, referent);
          }

          /**
           * Check if the given instance has associated subscriptions.  If yes, it can't be deleted implicitly.
           *
           * @param    resource
           *           The instance to check.
           *
           * @return   If there are any subscriptions registered.
           */
          public boolean hasSubscriptions(_BaseObject_ resource)
          {
             Function<List<Subscription>, Boolean> check = (subs) ->
             {
                Iterator<Subscription> iter = subs.iterator();
                while (iter.hasNext())
                {
                   Subscription sub = iter.next();

                   if (sub.subscriber.equals(resource))
                   {
                      return true;
                   }
                }

                return false;
             };

             if (check.apply(wa.subscriptions))
             {
                return true;
             }

             Session session = SessionManager.get().getSession();
             List<Subscription> subs = globalSubscriptions.subscriptions.get(session);

             if (subs != null && check.apply(subs))
             {
                return true;
             }

             return false;
          }

          /**
           * Get the <code>SUPER-PROCEDURES</code> attribute of the specified
           * external procedure instance.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   See above.
           */
          public character superProcedures(Object referent)
          {
             return superProceduresImpl(wa.getSuperProcedures(referent, true).values());
          }

          /**
           * Remove the given handle from the specified referent's list of
           * super-procedures.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           * @param    h
           *           The procedure handle to be removed.
           *
           * @return   <code>true</code> if <code>h</code> is a valid procedure
           *           handle.
           */
          public logical removeSuperProcedure(Object referent, handle h)
          {
             if (!h._isValid() || !isProcedure(h))
             {
                return new logical(false);
             }

             ControlFlowOps.invalidateCallSiteCache();

             ProcedureData pData = locateProcedure(wa, h.get());
             pData.addedToSuperProcedures.remove(referent);
             removeProcedureImpl(wa, h, wa.getSuperProcedures(referent, true).keySet());

             return new logical(true);
          }

          /**
           * Add the given handle as a super-procedure for the specified referent.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           * @param    h
           *           The handle to be added as a super-procedure.
           *
           * @return   <code>true</code> if <code>h</code> is a valid procedure
           *           handle.
           */
          public logical addSuperProcedure(Object referent, handle h)
          {
             return addSuperProcedure(referent, h, (SearchMode) null);
          }

          /**
           * Add the given handle as a super-procedure for the specified referent,
           * using the explicit {@link SearchMode mode}.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           * @param    h
           *           The handle to be added as a super-procedure.
           * @param    mode
           *           The {@link SearchMode} to be set for the <code>h</code>
           *           handle.
           *
           * @return   <code>true</code> if <code>h</code> is a valid procedure
           *           handle.
           */
          public logical addSuperProcedure(Object referent, handle h,SearchMode mode)
          {
             return ProcedureManager.addSuperProcedureImpl(wa, h, mode, referent);
          }

          /**
           * Add the given handle as a super-procedure for the specified referent,
           * using the search-mode set as a string (one of the
           * <code>SEARCH-TARGET</code> or <code>SEARCH-SELF</code> string literals).
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           * @param    h
           *           The handle to be added as a super-procedure.
           * @param    mode
           *           The search-mode to be set for the <code>h</code> handle.
           *
           * @return   <code>true</code> if <code>h</code> is a valid procedure
           *           handle.
           */
          public logical addSuperProcedure(Object referent, handle h, int mode)
          {
             return addSuperProcedure(referent, h, getSearchMode(mode));
          }

          /**
           * Resolve the instantiating procedure from the given handle: if the handle contains a valid
           * procedure, return a copy of it; else, return an unknown handle.
           *
           * @param    instProc
           *           The instance representing an instantiating procedure.
           *
           * @return   See above.
           */
          public handle resolveInstantiatingProcedure(Object instProc)
          {
             handle res = new handle();

             if (instProc != null && wa.pMap.containsKey(instProc))
             {
                res.assign(new ExternalProgramWrapper(this, instProc));
             }

             return res;
          }

          /**
           * Check if the given referent is the top-level procedure (the startup procedure).
           *
           * @param    referent
           *           The instance to be verified.
           *
           * @return   <code>true</code> if the referent is the startup procedure.
           */
          public boolean isTopLevel(Object referent)
          {
             return wa.thisProceduresData.getLast().getReferent() == referent;
          }

          /**
           * Set the <code>PRIVATE-DATA</code> attribute of the specified referent.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           * @param    data
           *           the PRIVATE-DATA.
           */
          public void setPrivateData(Object referent, character data)
          {
             ProcedureData pp = locateProcedure(wa, referent);
             pp.privateData = data.getValue();
          }

          /**
           * Get the <code>PRIVATE-DATA</code> attribute of the specified referent.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   See above.
           */
          public character getPrivateData(Object referent)
          {
             ProcedureData pp = locateProcedure(wa, referent);

             return pp.privateData == null ? new character() : new character(pp.privateData);
          }

          /**
           * Gets the the unique ID number associated to the referent. Not the
           * same as the handle value itself.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   The integer number of the children.
           */
          public integer getUniqueID(Object referent)
          {
             ProcedureData pp = locateProcedure(wa, referent);
             if (pp.uniqueID == null)
             {
                pp.uniqueID = UniqueIdGenerator.getUniqueId(IdKind.PROCEDURE);
             }

             return new integer(pp.uniqueID);
          }

          /**
           * Check if this resource is currently being implicitly deleted.
           *
           * @param    res
           *           The resource to check.
           *
           * @return   <code>true</code> if the resource is the same as
           *           {@link WorkArea#pendingResourceDelete}.
           */
          public boolean pendingResourceDelete(WrappedResource res)
          {
             return wa.pendingResourceDelete == res;
          }

          /**
           * Register the given resource as being created by the specified procedure.
           *
           * @param    referent
           *           The procedure which creates the resource.
           * @param    resource
           *           The resource being created.
           */
          public void registerResource(Object referent, HandleResource resource)
          {
             ProcedureData pdata = locateProcedure(wa, referent);
             if (pdata != null)
             {
                pdata.createdResources.put(resource, null);
             }
             else if (LOG.isLoggable(Level.FINE))
             {
                Long id = handle.resourceId(resource);
                LOG.log(Level.FINE, "Could not register resource " + resource.type() +
                                    " with id " + id + " - the data for procedure " +
                                     referent.getClass().getName() + " could not be found!");
             }
          }

          /**
           * Deregister the given resource from the specified creator procedure.
           *
           * @param    referent
           *           The procedure which creates the resource.
           * @param    resource
           *           The resource being created.
           */
          public void deregisterResource(Object referent, HandleResource resource)
          {
             ProcedureData pdata = locateProcedure(wa, referent);

             if (pdata != null)
             {
                pdata.createdResources.remove(resource);
             }
             else if (LOG.isLoggable(Level.FINE))
             {
                Long id = handle.resourceId(resource);
                LOG.log(Level.FINE, "Could not deregister resource " + resource.type() +
                                    " with id " + id + " - the data for procedure " +
                                     referent.getClass().getName() + " could not be found!");
             }
          }

          /**
           * Check if we are executing finalizable or scopeable notifications.
           *
           * @return   See above.
           */
          public boolean inNotifications()
          {
             return wa.processedProcedure != null;
          }

          /**
           * Check if the given referent is a valid procedure.
           *
           * @param    referent
           *           The instance to check.
           */
          public boolean isValid(Object referent)
          {
             return referent != null && wa.pMap.containsKey(referent);
          }

          /**
           * Get the legacy stack trace
           *
           * @return the legacy stack trace
           */

          public List<String> getStackTrace()
          {
             return ProcedureManager.getStackTrace(wa);
          }

          /**
           * Delete the specified resource - this allows a static resource to be deleted.
           *
           * @param    res
           *           The resource to delete.
           */
          public void deleteResource(WrappedResource res)
          {
             WrappedResource old = wa.pendingResourceDelete;
             wa.pendingResourceDelete = res;
             try
             {
                ((Deletable) res).delete();
             }
             finally
             {
                wa.pendingResourceDelete = old;
             }
          }

          /**
           * Gets the INSTANTIATING-PROCEDURE associated to the referent.
           *
           * @param    referent
           *           The external program.
           *
           * @return   See above.
           */
          public Object getInstantiatingProcedure(Object referent)
          {
             ProcedureData pdata = locateProcedure(wa, referent);
             return pdata.instProc == null ? null : pdata.instProc.get();
          }

          /**
           * Get the {@code NEXT-SIBLING} attribute of the specified referent.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   See above.
           */
          public handle getNextSibling(Object referent)
          {
             Object next = wa.persistentProcedures.getNext(referent);
             // exclude surrogate referents for OO instances, and any single-run/singleton proxy objects
             while (ObjectOps.isLegacyObject(next) || (next instanceof DeferredProgram))
             {
                next = wa.persistentProcedures.getNext(next);
             }
             if (next == null)
             {
                return new handle();
             }

             return locateProcedure(wa, next).phandle;
          }

          /**
           * Get the {@code PREV-SIBLING} attribute of the specified referent.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   See above.
           */
          public handle getPrevSibling(Object referent)
          {
             Object prev = wa.persistentProcedures.getPrevious(referent);
             // exclude surrogate referents for OO instances, and any single-run/singleton proxy objects
             while (ObjectOps.isLegacyObject(prev) || (prev instanceof DeferredProgram))
             {
                prev = wa.persistentProcedures.getPrevious(prev);
             }

             if (prev == null)
             {
                return new handle();
             }

             return locateProcedure(wa, prev).phandle;
          }

          /**
           * Check if the given referent is for a single-run procedure. This
           * implements the SINGLE-RUN attribute for a procedure handle.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   <code>true</code> if the referent is a single-run procedure.
           */
          public logical isSingleRun(Object referent)
          {
             return new logical(_isSingleRun(wa, referent));
          }

          /**
           * Check if the given referent is for a singleton procedure. This
           * implements the SINGLETON attribute for a procedure handle.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   <code>true</code> if the referent is a singleton procedure.
           */
          public logical isSingleton(Object referent)
          {
             return new logical(_isSingleton(wa, referent));
          }

          /**
           * Check if the given referent is for a persistent procedure. This
           * implements the PERSISTENT attribute for a procedure handle.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   <code>true</code> if the referent is a persistent procedure.
           */
          public logical isPersistent(Object referent)
          {
             ProcedureData pdata = locateProcedure(wa, referent);
             boolean persistent = pdata != null && pdata.persistent && !(pdata.singleRun || pdata.singleton);

             return  logical.of(persistent);
          }

          /**
           * Get the value of the ADM-DATA attribute of the specified referent.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   See above.
           */
          public character getADMData(Object referent)
          {
             ProcedureData pp = locateProcedure(wa, referent);
             return new character(pp.admData);
          }

          /**
           * Set the value of the ADM-DATA attribute for the specified referent.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @param    value
           *           The new value.
           */
          public void setADMData(Object referent, String value)
          {
             ProcedureData pp = locateProcedure(wa, referent);
             pp.admData = value;
          }

          /**
          * Get an iterator over the stack of currently executing code.
          *
          * @return    An iterator of Java classes associated with the code executing on the stack.
          */
          public Iterator<Class<?>> executingIterator()
          {
             return wa.executingIterator();
          }

          /**
          * Get an iterator over the stack of currently destination resolved code.
          *
          * @return    An iterator of Java classes associated with the code on the stack.
          */
          public Iterator<Object> destinationIterator()
          {
             return wa.destinationIterator();
          }

          /**
           * For procedures or functions defined IN SUPER, this computes the target procedure handle
           * where the procedure's or function's body resides.
           * <p>
           * In case when no super procedure is found, it will throw proper error messages and return
           * <code>null</code>.
           *
           * @param    iename
           *           Internal-entry's name (this may be a procedure or function).
           * @param    function
           *           <code>true</code> if this is a function.
           *
           * @return   A procedure handle where to search for the internal-entry's body or
           *           <code>null</code> of none found.
           */
          handle getSuperHandle(String iename, boolean function)
          {
             handle hthis           = ProcedureManager.thisProcedure(wa);
             handle htarget         = targetProcedure(wa);
             Object refThis         = hthis.get();
             Object refTarget       = htarget.get();
             ProcedureData ppThis   = locateProcedure(wa, refThis);
             ProcedureData ppTarget = locateProcedure(wa, refTarget);

             boolean searchSelf = ppThis.isSearchSelf();
             handle psuper = null;

             @SuppressWarnings("unchecked")
             Collection<Object>[] search = new Collection[2];

             if (searchSelf)
             {
                search[0] = ppThis.superProcedures.keySet();
                search[1] = wa.sessionSuperProcs.keySet();
             }
             else
             {
                Collection<Object> targetProcs = ppTarget.superProcedures.keySet();
                search[0] = new ArrayDeque<>(targetProcs);
                search[1] = new ArrayDeque<>(wa.sessionSuperProcs.keySet());

                boolean inTarget = search[0].contains(refThis);
                boolean inSess   = search[1].contains(refThis);

                if (inTarget || inSess)
                {
                   // remove referents until this-procedure is found in the list
                   prepareSearches(search, refThis);
                }

                if (!(inTarget || inSess) || (inSess && search[1].isEmpty() && !targetProcs.isEmpty()))
                {
                   if (!function)
                   {
                      // in some cases when invoking internal procedures, it can end up in an infinite
                      // recursion

                      search[0] = new ArrayDeque<>(targetProcs);
                      search[1] = new ArrayDeque<>(wa.sessionSuperProcs.keySet());

                      // step 1. find the first already-invoked proc part of target's super-procs
                      Object ref = null;
                      for (CalleeInfoImpl info : wa.calleeInfo)
                      {
                         if (targetProcs.contains(info.destination))
                         {
                            ref = info.destination;
                            break;
                         }

                         if (!info.superCall && refTarget != info.destination)
                         {
                            break;
                         }
                      }

                      if (ref != null)
                      {
                         // step 2. remove all until ref is found, from all searches
                         prepareSearches(search, ref);
                      }
                      else
                      {
                         search[0] = null;
                         search[1] = null;
                      }
                   }
                   else
                   {
                      // not found, clear the lists
                      search[0] = null;
                      search[1] = null;
                   }
                }
             }

             for (int i = 0; i < search.length && psuper == null; i++)
             {
                if (search[i] == null || search[i].isEmpty())
                {
                   continue;
                }

                // search is done backwards (last set super-procedure is searched first)
                Deque<Object> queue = new ArrayDeque<>(search[i]);
                while (!queue.isEmpty())
                {
                   Object referent = queue.removeLast();
                   ProcedureData pp = locateProcedure(wa, referent);

                   if (pp == null || !pp.phandle._isValid())
                   {
                      // deleted or invalid procedures are ignored
                      continue;
                   }

                   String spname = getAbsoluteName(pp.phandle.get());
                   String jname = SourceNameMapper.getMethodName(spname, iename, function);
                   if (jname != null)
                   {
                      // found it
                      psuper = pp.phandle;
                      break;
                   }
                }
             }

             if (psuper == null)
             {
                String err = null;
                int errId = -1;
                if (function)
                {
                   final String errMsg =
                      "SUPER version of user-defined function '%s' invoked but could not be found";
                   errId = 6454;
                   err = String.format(errMsg, iename);

                   ErrorManager.recordOrShowError(errId, err, false, false, false);
                }
                else
                {
                   String pname = getRelativeName(searchSelf ? refThis : refTarget);

                   errId = 6439;
                   if (searchSelf)
                   {
                      final String errMsg =
                         "Procedure %s %s has no SUPER procedure with internal procedure %s";
                      err = String.format(errMsg, iename, pname, iename);
                   }
                   else
                   {
                      final String errMsg =
                        "Procedure %s has no SUPER procedure with internal procedure %s";
                      err = String.format(errMsg, pname, iename);
                   }

                   ErrorManager.recordOrThrowError(errId, err, false);
                }
             }

             return psuper;
          }

          /**
           * Get the Java class associated with the currently executing code.
           *
           * @return   See above.
           */
          public Class<?> getExecuting()
          {
             return wa.getExecuting();
          }

          /**
           * Calculate the number of occurences of the specified referent, on current stack.
           *
           * @param    referent
           *           The referent to check.
           *
           * @return   The number of occurrences of the referent on the current stack.
           */
          public long onStackReferences(Object referent)
          {
             return wa.thisProceduresData.stream().filter(pd -> pd.referent == referent).count();
          }

          /**
           * Walk the {@link WorkArea#thisProceduresData THIS-PROCEDURE} stack and apply the search function;
           * return the first value given by the search function, once it returns a non-null value.
           *
           * @param    search
           *           The search function.
           *
           * @return   The first non-null value returned by the search function.
           */
          public Object searchInStack(Function<Object, Object> search)
          {
             return wa.searchInStack(search);
          }

          /**
           * Check if an external program was ran persistent by a remote side.
           *
           * @param   referent
           *          The external program instance.
           *
           * @return  <code>true</code> if the referent is part of the
           *          {@link WorkArea#remotePersistentProcedures} map.
           */
          public boolean isRemotePersistentProcedure(Object referent)
          {
             return wa.isRemotePersistentProcedure(referent);
          }

          /**
           * Register an external program which was ran persistent by a remote side.
           *
           * @param   referent
           *          The external program instance.
           */
          public void registerRemotePersistentProcedure(Object referent)
          {
             wa.remotePersistentProcedures.add(referent);
          }

          /**
           * Get the THIS-PROCEDURE system handle.
           *
           * @return   See above.
           */
          public handle thisProcedure()
          {
             return ProcedureManager.thisProcedure(wa);
          }

          /**
           * Get the THIS-PROCEDURE referent.
           *
           * @return   See {@link #thisProcedure()}.
           */
          public Object _thisProcedure()
          {
             return ProcedureManager._thisProcedure(wa);
          }

          /**
           * Get the root procedure instance .
           */
          public Object _rootProcedure()
          {
             return wa.thisProceduresData.peekLast().getReferent();
          }

          /**
           * Check if {@link #_thisProcedure()} is ran persistent.
           *
           * @return   <code>true</code> if THIS-PROCEDURE is ran persistent.
           */
          public boolean isThisProcedurePersistent()
          {
             return wa.thisProceduresData.peek().persistent;
          }

          /**
           * Get the THIS-PROCEDURE data for the previous block on the stack.
           *
           * @return   See above.
           */
          public ProcedureData prevThisProcedureData()
          {
             ProcedureData pdata = wa.thisProceduresData.pop();
             try
             {
                return wa.thisProceduresData.peek();
             }
             finally
             {
                wa.thisProceduresData.push(pdata);
             }
          }
          /**
           * Get the THIS-PROCEDURE data for the current block.
           *
           * @return   See above.
           */
          public ProcedureData thisProcedureData()
          {
             return wa.thisProceduresData.peek();
          }

          /**
           * Get the SOURCE-PROCEDURE referent.
           *
           * @return   See {@link #sourceProcedure()}.
           */
          public Object _sourceProcedure()
          {
             return wa.sourceProceduresData.peek().getReferent();
          }

          /**
           * Check if the given referent is for a persistent procedure. This implements the PERSISTENT
           * attribute for a procedure handle.
           *
           * @param    referent
           *           An instance of a converted external procedure class.
           *
           * @return   <code>true</code> if the referent is a persistent procedure.
           */
          public boolean _isPersistent(Object referent)
          {
             return ProcedureManager._isPersistent(wa, referent);
          }

          /**
           * Check if the given referent is for a persistent procedure which has not been deleted explicitly while
           * its still on stack, so a delete is pending when last call for this reference exits the stack.
           *
           * @param    referent
           *           An instance of a converted external procedure class.
           *
           * @return   <code>true</code> if the referent is a persistent procedure.
           */
          public boolean _isPersistentAndNotPendingDelete(Object referent)
          {
             if (!ProcedureManager._isPersistent(wa, referent))
             {
                return false;
             }

             if (_isDelayedDelete(referent))
             {
                // check that is not still on the stack - a persistent procedure which had a
                // 'DELETE PROCEDURE THIS-PROCEDURE' executed will have its delete postponed until the reference
                // exits the stack

                // exclude the current block - on scope notification, 'this-procedure' is popped after all other
                // scopeables were notified.
                Iterator<ProcedureData> iter = wa.thisProceduresData.iterator();
                Object next = iter.next().getReferent(); // skip THIS-PROCEDURE
                while (iter.hasNext())
                {
                   next = iter.next().getReferent();
                   if (next == referent)
                   {
                      return true;
                   }
                }

                return false;
             }
             else
             {
                // this is a plain old persistent procedure
                return true;
             }
          }

          /**ProcedureHelper
           * Get the name of the currently instantiated external program.
           *
           * @return   The 4GL external program name being instantiated, or <code>null</code> if this is
           *           not the case.
           */
          public String getInstantiatingExternalProgram()
          {
             return wa.initiatingExtProg;
          }

          /**
           * Get the source class of the currently instantiating external program or OE class.
           *
           * @return   The Java class associated with the source code for the current instantiating OE
           *           class.
           */
          public Class<?> getInstantiatingExternalProgramClass()
          {
             return wa.initiatingExtProgClass;
          }

          /**
           * Set the external procedure beeing processed for scopeable or finalizable notifications.
           *
           * @param    referent
           *           The external procedure instance.
           */
          public void setProcessedProcedure(Object referent)
          {
             wa.processedProcedure = referent;
          }

          /**
           * Get the current external program (an application logic instance) being processed for
           * scopeable or finalizable notifications.
           *
           * @return   See above
           */
          public Object getProcessedProcedure()
          {
             return wa.processedProcedure;
          }

          /**
           * Cleanup of pending resources. This is called when the external procedure constructor failed
           * to build a new object because of an exception thrown in a member initialization (shared
           * frame/table).
           */
          public void cleanupPending()
          {
             // a no-op in case of headless mode
             if (wa.tm == null || wa.tm.errHlp == null || wa.tm.errHlp.isHeadless())
             {
                return;
             }

             wa.pendingResources.clear();
          }

          /**
           * Check if this is a "delete-on-exit" procedure.
           *
           * @param    referent
           *           An instance of a converted external procedure class.
           *
           * @return   <code>true</code> if the referent is a "delete-on-exit" procedure.
           */
          public boolean _isDelayedDelete(Object referent)
          {
             Boolean delete = wa.deleteOnExit.get(referent);

             return delete != null && delete;
          }

          /**
           * Save the information about the program being called, to be used on the top-level scope
           * start notification: at that time, the info about the callee (this, source- and target-
           * procedures and other info) will be resolved.
           *
           * @param    def
           *           The currently executing OE class or external program.
           * @param    superCall
           *           Flag indicating if the destination is in a super-procedure.
           * @param    target
           *           The target procedure, which was targeted by the caller.
           * @param    destination
           *           The procedure where the internal entry was resolved.
           * @param    iename
           *           The internal entry name. In case of triggers or external programs, use the
           *           {@link #EXTERNAL_PROGRAM} and {@link #TRIGGER_PROCEDURE} constants.
           * @param    pname
           *           The external program name; will be non-null only when we are invoking an external
           *           program.
           * @param    function
           *           Flag indicating we are calling a user-defined function.
           * @param    persistentProc
           *           Flag indicating the external program was ran persistent.
           */
          public void pushCalleeInfo(Class<?> def,
                                     boolean  superCall,
                                     Object   target,
                                     Object   destination,
                                     String   iename,
                                     String   pname,
                                     boolean  function,
                                     boolean  persistentProc)
          {
             pushCalleeInfo(def, superCall, target, destination, iename, pname, function, persistentProc, false);
          }

          /**
           * Save the information about the program being called, to be used on the top-level scope
           * start notification: at that time, the info about the callee (this, source- and target-
           * procedures and other info) will be resolved.
           *
           * @param    def
           *           The currently executing OE class or external program.
           * @param    superCall
           *           Flag indicating if the destination is in a super-procedure.
           * @param    target
           *           The target procedure, which was targeted by the caller.
           * @param    destination
           *           The procedure where the internal entry was resolved.
           * @param    iename
           *           The internal entry name. In case of triggers or external programs, use the
           *           {@link #EXTERNAL_PROGRAM} and {@link #TRIGGER_PROCEDURE} constants.
           * @param    pname
           *           The external program name; will be non-null only when we are invoking an external
           *           program.
           * @param    function
           *           Flag indicating we are calling a user-defined function.
           * @param    persistentProc
           *           Flag indicating the external program was ran persistent.
           * @param    dynamicCall
           *           Flag indicating a dynamic call.
           */
          public void pushCalleeInfo(Class<?> def,
        		  					 boolean  superCall,
        		  					 Object   target,
        		  					 Object   destination,
        		  					 String   iename,
        		  					 String   pname,
        		  					 boolean  function,
        		  					 boolean  persistentProc,
        		  					 boolean  dynamicCall)
          {
        	   CalleeInfoImpl info = new CalleeInfoImpl(wa, def, superCall, target, destination,
                                   					    iename, pname, function,
                                   					    persistentProc, false, false, dynamicCall);
        	   wa.calleeInfo.push(info);
          }

          /**
           * Save the information about the program being called, to be used on the top-level scope
           * start notification: at that time, the info about the callee (this, source- and target-
           * procedures and other info) will be resolved.
           *
           * @param    def
           *           The currently executing OE class or external program.
           * @param    superCall
           *           Flag indicating if the destination is in a super-procedure.
           * @param    target
           *           The target procedure, which was targeted by the caller.
           * @param    destination
           *           The procedure where the internal entry was resolved.
           * @param    iename
           *           The internal entry name. In case of triggers or external programs, use the
           *           {@link #EXTERNAL_PROGRAM} and {@link #TRIGGER_PROCEDURE} constants.
           * @param    pname
           *           The external program name; will be non-null only when we are invoking an external
           *           program.
           * @param    function
           *           Flag indicating we are calling a user-defined function.
           * @param    persistentProc
           *           Flag indicating the external program was ran persistent.
           * @param    singletonProc
           *           Flag indicating the external program was ran singleton.
           * @param    singleRunProc
           *           Flag indicating the external program was ran single-run.
           */
          public void pushCalleeInfo(Class<?> def,
                                     boolean superCall,
                                     Object  target,
                                     Object  destination,
                                     String  iename,
                                     String  pname,
                                     boolean function,
                                     boolean persistentProc,
                                     boolean singletonProc,
                                     boolean singleRunProc)
          {
             CalleeInfoImpl info = new CalleeInfoImpl(wa,
                                                      def, superCall, target, destination,
                                                      iename, pname, function,
                                                      persistentProc, singletonProc,
                                                      singleRunProc, false);
             wa.calleeInfo.push(info);
          }

          /**
           * Pop the last callee info from the stack.
           */
          public void popCalleeInfo()
          {
             wa.calleeInfo.pop();
          }

          /**
           * Return the last callee info from the stack.
           *
           * @return  {@link CalleeInfo} instance.
           */
          public CalleeInfo peekCalleeInfo()
          {
             return wa.calleeInfo.peek();
          }

          /**
           * Execute the given block with THIS-PROCEDURE as the given instProc procedure.
           *
           * @param    thisProcedureOverride
           *           Procedure to be treated as the new THIS-PROCEDURE.
           * @param    block
           *           The code to execute.
           */
          public void executeInThisProcedure(Object thisProcedureOverride, Block block)
          {
             boolean pushThisProcedure = _thisProcedure() != thisProcedureOverride;
             try
             {
                if (pushThisProcedure)
                {
                   wa.thisProceduresData.push(wa.pMap.get(thisProcedureOverride));
                   wa.internalEntryStack.push(THIS_PROCEDURE_OVERRIDE);
                }

                block.body();
             }
             finally
             {
                if (pushThisProcedure)
                {
                   wa.thisProceduresData.pop();
                   wa.internalEntryStack.pop();
                }
             }
          }

          /**
           * Get the relative name used to create the given external program instance.
           *
           * @param    instance
           *           The external program instance.
           *
           * @return   See above.
           */
          public String getRelativeName(Object instance)
          {
             return wa.relativeNames.get(instance);
          }

          /**
           * Check if the given referent has been (or is still) registered with the
           * {@link ProcedureManager} in the {@link WorkArea#pMap procedure} map.
           *
           * @param     referent
           *            The referent to be checked if is still loaded.
           *
           * @return    <code>true</code> if the referent was found.
           */
          public boolean hasReferent(Object referent)
          {
             return ProcedureManager.hasReferent(wa, referent);
          }

          /**
           * Check if a provided referent is stale or not.
           *
           * @param    referent
           *           The referent for which the checking will be done
           *
           * @return   {@code true} if the referent is stale.
           */
          public boolean hasStaleReferent(Object referent)
          {
             return ProcedureManager.hasStaleReferent(wa, referent);
          }

          /**
           * Check whether the specified referent (procedure) has been executed or not.
           *
           * @param    referent
           *           The referent which is checked for staleness
           *
           * @return   {@code true} if the procedure has been executed or was not found (i.e., is assumed to have
           *           executed and been deleted/discarded); else {@code false}.
           */
          public boolean isExecuted(Object referent)
          {
             return ProcedureManager.isExecuted(wa, referent);
          }

          /**
           * A system which notifies the ProcedureManager when a static record buffer is changed.
           * This is used to keep up to date the persistent procedure stacks - which are in fact
           * snapshots of the block stacks stored the first time a static record is loaded.
           *
           * @param     recordBuffer
           *            The record buffer which was loaded and triggered this notification.
           */
          public void notifyStaticRecordWasLoaded(RecordBuffer recordBuffer)
          {
             Object targetProcedure = recordBuffer.getPersistentProc();
             if (targetProcedure == null                   ||
                 ObjectOps.isLegacyObject(targetProcedure) ||
                 wa.hasPersistentProcStack(targetProcedure))
             {
                // legacy OO does not have the 'stale procedure' behavior.
                return;
             }

             Stack<BlockDefinition> currentStack = wa.tm.copyStack();
             wa.addPersistentProcStack(targetProcedure, currentStack);
          }

          /**
           * Removes the referent information from the persistent procedure stack mapping
           * and stale set. This should be done only when the referent is deleted.
           *
           * @param    referent
           *           The persistent procedure for which the entry should be removed.
           */
          public void removeStaleProcedure(Object referent)
          {
             wa.removeStaleProcedure(referent);
          }

          /**
           * Obtains the stack snapshot of one persistent procedure. The stack represent the block
           * stack when the first global buffer was defined inside the referent.
           *
           * @param    referent
           *           The persistent procedure for which the stack should be retrieved.
           *
           * @return   The block stack snapshot stored for the specified referent.
           *
           */
          public Stack<BlockDefinition> getPersistentProcStack(Object referent)
          {
             return wa.getPersistentProcStack(referent);
          }

          /**
           * Mark that the selected referent is stale.
           *
           * @param    referent
           *           The referent which should be marked as stale.
           */
          public boolean addStaleReferent(Object referent)
          {
             return wa.addStaleReferent(referent);
          }

          /**
           * Set the name of the 4GL program currently being instantiated, into the
           * {@link WorkArea#initiatingExtProg} field.
           *
           * @param    name
           *           The 4GL external program name.
           */
          public void setInstantingExternalProgram(String name)
          {
             wa.setInstantingExternalProgram(name);
          }

          /**
           * Set the Java class of the currently instantiating external program or OE class.
           *
           * @param    cls
           *           The Java class for the currently executing code.
           */
          public void setInstantingExternalProgramClass(Class<?> cls)
          {
             wa.setInstantingExternalProgramClass(cls);
          }

          /**
           * Registers a specific procedure-cleanup handler. The code will be executed AFTER the last bit of code of
           * the CURRENT procedure was executed, including the eventual {@code fini} / {@code finally:} block, just
           * before the procedure is popped out from the stack.
           *
           * @param   code
           *          The code that does the cleanup. It can handle a (input-)output parameter or other resource
           *          whose scope is bound to the current procedure. For example, in case of a table parameter, it
           *          will handle the unbinding process of the table parameter from the local table.
           * @param   weight
           *          The priority of the code. The lower the weight, the higher priority.
           */
          public void executeOnReturn(Runnable code, WeightFactor weight)
          {
             Deque<List<Runnable>[]> allHandlers = wa.cleanupHandlers;
             List<Runnable>[] handlers = allHandlers.peek();
             if (handlers == null)
             {
                LOG.log(Level.WARNING, "No procedure scope was defined at this moment");
                return;
             }

             if (handlers == WorkArea.EMPTY_SCOPE_STACK)
             {
                // replace the empty scope marker with an actual container for the handlers
                allHandlers.pop();
                handlers = new ArrayList[WeightFactor.WEIGHTS_ARRAY_SIZE];
                allHandlers.push(handlers);
             }

             List<Runnable> handlerSusbset = handlers[weight.getIndex()];
             if (handlerSusbset == null)
             {
                handlerSusbset = new ArrayList<>();
                handlers[weight.getIndex()] = handlerSusbset;
             }

             handlerSusbset.add(code);
          }

          /**
           * Deregister the given code from the {@link WorkArea#cleanupHandlers}.
           *
           * @param   code
           *          The clean code that needs to be removed.
           * @param   weight
           *          The priority of the code. The lower the weight, the higher priority.
           */
          public void deregisterExecuteOnReturn(Runnable code, WeightFactor weight)
          {
             Deque<List<Runnable>[]> allHandlers = wa.cleanupHandlers;
             List<Runnable>[] handlers = allHandlers.peek();
             if (handlers == null)
             {
                LOG.log(Level.WARNING, "No procedure scope was defined at this moment");
                return;
             }

             if (handlers != WorkArea.EMPTY_SCOPE_STACK)
             {
                List<Runnable> handlerSubset = handlers[weight.getIndex()];
                if (handlerSubset != null)
                {
                   handlerSubset.remove(code);
                }
             }
          }

          /**
           * Using the name of a function defined IN handle and a procedure handle
           * to which this function definition belongs, resolve the target handle,
           * to be able to invoke it.
           *
           * @param    phandle
           *           The procedure handle where the function was defined.
           * @param    fname
           *           The function's legacy name.
           *
           * @return   The procedure handle where to search for the function's body.
           */
          handle getHandleForFunction(handle phandle, String fname)
          {
             Object referent = phandle.get();
             ProcedureData pp = locateProcedure(wa, referent);

             Resolvable target = pp.inHandleFunctions.get(fname.toLowerCase());
             if (target == null && _BaseObject_.class.isAssignableFrom(referent.getClass()))
             {
                // search for a registration in the legacy class
                referent = ObjectOps.getStaticInstance((Class<? extends _BaseObject_>) referent.getClass());
                pp = locateProcedure(wa, referent);
                target = pp.inHandleFunctions.get(fname.toLowerCase());
             }
             handle h = target == null ? null : (handle) target.resolve();
             return h == null ? new handle() : h;
          }

          /**
           * Add the <code>h</code> handle as a persistent procedure. Used by
           * {@link ControlFlowOps#invokePersistent(character, Object...)} and
           * {@link ControlFlowOps#invokePersistentSet(String, handle, Object...)}.
           *
           * @param   h
           *          The procedure handle to be added to the list of persistent
           *          procedures.
           * @param   name
           *          The name of this procedure, used at the RUN statement.
           * @param   persistent
           *          Flag indicating if this is a persistent procedure.
           */
          void addProcedure(handle h, String name, boolean persistent)
          {
             ProcedureManager.addProcedure(wa, h, name, persistent, false, false, false);
          }

          /**
           * Add the <code>h</code> handle as a persistent procedure. Used by
           * {@link ControlFlowOps#invokePersistent(character, Object...)} and
           * {@link ControlFlowOps#invokePersistentSet(String, handle, Object...)}.
           *
           * @param   h
           *          The procedure handle to be added to the list of persistent
           *          procedures.
           * @param   name
           *          The name of this procedure, used at the RUN statement.
           * @param   persistent
           *          Flag indicating if this is a persistent procedure.
           * @param   singleton
           *          Flag indicating if this is a singleton procedure.
           * @param   singleRun
           *          Flag indicating if this is a single-run procedure.
           * @param   legacyClass
           *          Set to {@code true} if the method is called as a result of
           *          legacy class initialization. {@code false} otherwise.
           */
          void addProcedure(handle h,
                            String name,
                            boolean persistent,
                            boolean singleton,
                            boolean singleRun,
                            boolean legacyClass)
          {
             ProcedureManager.addProcedure(wa, h, name, persistent, singleton, singleRun, legacyClass);
          }

          /**
           * Add the <code>h</code> handle as a persistent procedure. Used by
           * {@link ControlFlowOps#invokePersistent(character, Object...)} and
           * {@link ControlFlowOps#invokePersistentSet(String, handle, Object...)}.
           *
           * @param   h
           *          The procedure handle to be added to the list of persistent
           *          procedures.
           * @param   name
           *          The name of this procedure, used at the RUN statement.
           * @param   singleton
           *          Flag indicating if this is a singleton procedure.
           * @param   singleRun
           *          Flag indicating if this is a singleton procedure.
           */
          void addProcedure(handle h, String name, boolean singleton, boolean singleRun)
          {
             ProcedureManager.addProcedure(wa, h, name, false, singleton, singleRun, false);
          }

          /**
           * Implementation for the PROGRAM-NAME functions. Resolves the legacy top level block (external
           * program, function, internal procedure or trigger), at the given depth. The index starts with
           * 0 for the currently executing top-level block.
           *
           * @param    idx
           *           The stack entry index.
           *
           * @return   The name of the stack entry or {@code null} if outside of stack.
           */
          String getStackEntry(int idx)
          {
             return wa.getStackEntry(idx);
          }

          /**
           * Get the currently executing internal entry name.
           *
           * @return   The top of the {@link WorkArea#internalEntryStack}.
           */
          String currentInternalEntry()
          {
             return wa.internalEntryStack.peek();
          }

          /**
           * Delete the given procedure handle (i.e. execute the DELETE OBJECT or DELETE PROCEDURE
           * handle).
           *
           * @param    referent
           *           The persistent procedure instance to be deleted.
           * @param    letStaleProcedure
           *           Flag to indicate if the procedure should go in an stale state.
           *
           * @return   <code>true</code> if this procedure handle can be deleted.
           */
          boolean delete(Object referent, boolean letStaleProcedure)
          {
             return ProcedureManager.delete(wa, referent, letStaleProcedure);
          }

          /**
           * Check if current THIS-PROCEDURE is marked as ROUTINE-LEVEL UNDO, THROW or
           * BLOCK-LEVEL UNDO, THROW (as BLOCK-LEVEL includes the ROUTINE-LEVEL blocks).
           *
           * @return   See above.
           */
          boolean isRoutineLevelUndoThrow()
          {
             Object referent = _thisProcedure();
             ProcedureData pdata = wa.pMap.get(referent);
             return pdata.routineLevelUndoThrow || pdata.blockLevelUndoThrow;
          }

          /**
           * Check if current THIS-PROCEDURE is marked as BLOCK-LEVEL UNDO, THROW.
           *
           * @return   See above.
           */
          boolean isBlockLevelUndoThrow()
          {
             Object referent = _thisProcedure();
             ProcedureData pdata = wa.pMap.get(referent);
             return pdata.blockLevelUndoThrow;
          }

          /**
           * Mark current THIS-PROCEDURE as ROUTINE-LEVEL UNDO, THROW.
           */
          void markRoutineLevelUndoThrow()
          {
             Object referent = _thisProcedure();
             ProcedureData pdata = wa.pMap.get(referent);
             pdata.routineLevelUndoThrow = true;
          }

          /**
           * Mark current THIS-PROCEDURE as BLOCK-LEVEL UNDO, THROW.
           */
          void markBlockLevelUndoThrow()
          {
             Object referent = _thisProcedure();
             ProcedureData pdata = wa.pMap.get(referent);
             pdata.blockLevelUndoThrow = true;
          }

          /**
           * Register the given {@link Scopeable} to be processed when the persistent procedure gets
           * deleted.
           *
           * @param    referent
           *           The external program.
           * @param    target
           *           The {@link Scopeable} instance which needs delayed notification.
           */
          void addScopeable(Object referent, Scopeable target)
          {
             ProcedureData pdata = locateProcedure(wa, referent);
             if (pdata.scopeables == Collections.EMPTY_SET)
             {
                pdata.scopeables = Collections.newSetFromMap(new IdentityHashMap<>());
             }
             pdata.scopeables.add(target);
          }

          /**
           * Register the given {@link Finalizable} to be processed when the persistent procedure gets
           * deleted.
           *
           * @param    referent
           *           The external program.
           * @param    target
           *           The {@link Finalizable} instance which needs delayed notification.
           */
          void addFinalizable(Object referent, Finalizable target)
          {
             ProcedureData pdata = locateProcedure(wa, referent);
             if (pdata.finalizables == Collections.EMPTY_SET)
             {
                pdata.finalizables = Collections.newSetFromMap(new IdentityHashMap<>());
             }
             pdata.finalizables.add(target);
          }

          /**
           * Get a handle list of super-procedures for the given referent.
           *
           * @param    referent
           *           An instance of an converted external procedure class.
           *
           * @return   See above.
           */
          List<handle> getSuperProcedures(Object referent)
          {
             Map<Object, ProcedureData> supers = wa.getSuperProcedures(referent, true);
             if (supers.isEmpty())
             {
                return Collections.emptyList();
             }

             List<handle> superProcs = new LinkedList<>();
             // need to emit them in reverse order (they are kept in LIFO)
             for (ProcedureData pp : supers.values())
             {
                superProcs.add(0, pp.phandle);
             }

             return superProcs;
          }

          /**
           * Get a handle list of super-procedures for the session handle.
           *
           * @return   See above.
           */
          List<handle> getSuperProcedures()
          {
             return getSuperProcedures(null);
          }
       }

       /**
        * Container for a PUBLISH task to be executed in the actual subscription's context.
        */
       private static class PublishTask
       {
          /** The subscription on which {@link Subscription#session context } the task is executed. */
          private final Subscription subscription;

          /** The PUBLISH task. */
          private final Runnable task;

          /**
           * Create a new task.
           *
           * @param   subscription
           *          The subscription.
           * @param   task
           *          The PUBLISH task.
           */
          public PublishTask(Subscription subscription, Runnable task)
          {
             this.subscription = subscription;
             this.task = task;
          }
       }

       /**
        * Helper class to manage the global subscriptions.
        */
       private static class GlobalEventSubscriptions
       {
          /** The global list of subscriptions. */
          private final Map<Session, List<Subscription>> subscriptions = new HashMap<>();

          /** Map of session listener for each subscription. */
          private final Map<Subscription, SessionListener> listeners = new HashMap<>();

          /** Tasks for PUBLISH are posted here. */
          private Queue<PublishTask> queue = new LinkedList<>();

          /** Flag indicating the listening loop was terminated. */
          private boolean terminated = false;

          /**
           * Create a dedicated thread, in the server's context, which will {@link #listen()} for
           * PUBLISH tasks.
           */
          public GlobalEventSubscriptions()
          {
             String jobName = "global-publisher";
             Runnable launchJob = () ->
             {
                new AssociatedThread(new Runnable()
                {
                   @Override
                   public void run()
                   {
                      listen();
                   }
                })
                {
                   {
                      this.setName("Global Publisher Worker");
                      this.setDaemon(true);
                   };
                }.start();
             };

             Job job = new Job(jobName, launchJob, true, JobMode.NOW);
             if (!Scheduler.addJob(job))
             {
                throw new IllegalStateException(
                   "A job with for " + jobName + " is already configured!");
             }
          }

          /**
           * Listen for incoming {@link PublishTask tasks}, on the {@link #queue}.
           * <p>
           * Once a task is received, an {@link AssociatedThread} will be created and will execute the
           * task in the {@link Subscription#session subscription's session}, using
           * {@link SessionManager#executeInContext}.
           */
          private void listen()
          {
             SessionManager sm = SessionManager.get();

             ArrayList<PublishTask> tasks = new ArrayList<>();

             while (true)
             {
                tasks.clear();

                synchronized (queue)
                {
                   if (queue.isEmpty())
                   {
                      try
                      {
                         queue.wait();
                      }
                      catch (InterruptedException e)
                      {
                         LOG.log(Level.INFO, "global publish event listening loop interrupted");

                         // terminate the loop
                         terminated = true;
                         break;
                      }
                   }

                   tasks.addAll(queue);
                   queue.clear();
                }

                for (int i = 0; i < tasks.size(); i++)
                {
                   PublishTask task = tasks.get(i);
                   Runnable ptask = () ->
                   {
                      Thread t = Thread.currentThread();

                      try
                      {
                         sm.registerAsyncThread(t);

                         task.task.run();
                      }
                      finally
                      {
                         sm.deregisterAsyncThread(t);
                      }
                   };

                   try
                   {
                      sm.executeInContext(task.subscription.session,
                                          () -> new AssociatedThread(ptask).start());
                   }
                   catch (RestrictedUseException e)
                   {
                      String msg = "Not allowed to use SessionManager.executeInContext";
                      throw new IllegalStateException(msg, e);
                   }
                }
             }
          }

          /**
           * Post the given task to the {@link #queue}.
           *
           * @param    task
           *           The task for a PUBLISH event.
           */
          public void post(PublishTask task)
          {
             synchronized (queue)
             {
                if (terminated)
                {
                   LOG.log(Level.INFO,
                          "global publish event listening loop is terminated - could not post event");
                   return;
                }

                queue.offer(task);
                queue.notify();
             }
          }

          /**
           * Get all the global subscriptions, for all contexts.
           *
           * @return   See above.
           */
          public synchronized List<Subscription> getAllSubscriptions()
          {
             if (subscriptions.isEmpty())
             {
                return Collections.EMPTY_LIST;
             }

             List<Subscription> subs = new LinkedList<>();
             subscriptions.values().forEach(l -> subs.addAll(l));
             return subs;
          }

          /**
           * Remove all the global subscriptions for the given resource.  This will remove them only
           * from the current context's global subscriptions.
           *
           * @param    resource
           *           The resource which will be matched against the {@link Subscription#subscriber}.
           */
          public synchronized void removeAll(Object resource)
          {
             if (subscriptions.isEmpty())
             {
                return;
             }

             Session session = SessionManager.get().getSession();
             List<Subscription> subs = subscriptions.get(session);

             if (subs == null)
             {
                return;
             }

             Iterator<Subscription> iter = subs.iterator();
             while (iter.hasNext())
             {
                Subscription sub = iter.next();

                if (sub.subscriber.equals(resource))
                {
                   session.removeSessionListener(listeners.remove(sub));
                   iter.remove();
                }
             }

             if (subs.isEmpty())
             {
                subscriptions.remove(session);
             }
          }

          /**
           * Remove the specified subscription from the current context's global subscriptions.
           *
           * @param    subscription
           *           The subscription which will removed.
           */
          public synchronized void removeAll(Subscription subscription)
          {
             if (subscriptions.isEmpty())
             {
                return;
             }

             Session session = SessionManager.get().getSession();
             List<Subscription> subs = subscriptions.get(session);

             Iterator<Subscription> iter = subs.iterator();
             while (iter.hasNext())
             {
                Subscription sub = iter.next();

                if (sub == subscription)
                {
                   session.removeSessionListener(listeners.remove(sub));
                   iter.remove();
                }
             }

             if (subs.isEmpty())
             {
                subscriptions.remove(session);
             }
          }

          /**
           * Get all the global subscriptions, for this context.
           *
           * @return   See above.
           */
          public synchronized List<Subscription> getSubscriptions()
          {
             if (subscriptions.isEmpty())
             {
                return Collections.EMPTY_LIST;
             }

             Session session = SessionManager.get().getSession();
             List<Subscription> subs = subscriptions.get(session);

             return subs == null ? Collections.EMPTY_LIST
                                 : new ArrayList<>(subscriptions.get(session));
          }

          /**
           * Register the specified {@link Subscription} as global, for this context.
           *
           * @param    subscription
           *           The subscription to register.
           */
          public synchronized void subscribe(Subscription subscription)
          {
             Session session = SessionManager.get().getSession();

             List<Subscription> subs = subscriptions.get(session);
             if (subs == null)
             {
                subs = new ArrayList<>();
                subscriptions.put(session, subs);
             }

             SessionListener listener = new SessionListener()
             {
                @Override
                public void terminate(Session session)
                {
                   synchronized (GlobalEventSubscriptions.this)
                   {
                      subscriptions.remove(session);
                      listeners.remove(subscription);
                   }
                }

                @Override
                public void initialize(Session session)
                {
                   // no-op
                }
             };

             session.addSessionListener(listener);
             listeners.put(subscription, listener);

             subs.add(subscription);
          }
       }

       /**
        * Stores global data relating to the state of the current context.
        */
       private static class WorkArea
       implements Initializable,
                  Scopeable
       {
          /** The default databases for each java package prefix. */
          private Map<String, Set<String>> defaultDbs = null;

          /** Helper to use the TM without any context local lookups. */
          private TransactionManager.TransactionHelper tm = null;

          /** Helper to use the {@link WidgetPool} without any context local lookups. */
          private WidgetPool.WidgetPoolHelper wp = null;

          /** Helper to use the {@link ErrorManager} without any context local lookups. */
          private ErrorManager.ErrorHelper errHelper = null;

          /** Helper to use the {@link handle} without any context local lookups. */
          private handle.Helper h = null;

          /** Helper to use the {@link ObjectOps} without any context local lookups. */
          private ObjectOps.ObjectHelper oh = null;

          /** The list of all registered persistent procedures, via RUN PERSISTENT calls. */
          private FastSiblingDeque<Object> persistentProcedures = new FastSiblingDeque<>(true);

          /** A map containing the list of super-procedures, for the session, by each instance. */
          private LinkedHashMap<Object, ProcedureData> sessionSuperProcs = new LinkedHashMap<>();

          /** This map holds all the singleton instances in this context. */
          private IdentityHashMap<Class<?>, Object> singletonMap = new IdentityHashMap<>();

          /**
           * This map holds the attributes for all registered external procedures, by instance.
           * It includes all persistent procedures and all procedures active on the stack.
           */
          private IdentityHashMap<Object, ProcedureData> pMap = new IdentityHashMap<>();

          /**
           * Stack of target-procedures' data. Must be kept in sync with targetProcedures!
           * This stack holds procedure data for deleted TARGET-PROCEDURE handles.
           */
          private ArrayDeque<ProcedureData> targetProceduresData = new ArrayDeque<>();

          /**
           * Stack of source-procedures' data. Must be kept in sync with sourceProcedures!
           * This stack holds procedure data for deleted SOURCE-PROCEDURE handles.
           */
          private ArrayDeque<ProcedureData> sourceProceduresData = new ArrayDeque<>();

          /** Stack of this-procedures' data. Must be kept in sync with thisProcedures! */
          private ArrayDeque<ProcedureData> thisProceduresData = new ArrayDeque<>();

          /**
           * Stack with output parameter handlers to be processed on procedure's return. Each scope is created
           * using the {@code EMPTY_SCOPE_STACK} immutable object to avoid memory consumption due unused new
           * objects. This will be replaced with a true stack at the moment of first usage. We use a deque/stack
           * to enforce the order of execution of the cleanup handlers to be in reverse order, the LIFO order
           * assuring the scope bracketing inside nested scopes.
           */
          private final ArrayDeque<List<Runnable>[]> cleanupHandlers = new ArrayDeque<>();

          /** An immutable marker for UNUSED stack of cleanup handlers for a scope. */
          private static final LinkedList<Runnable>[] EMPTY_SCOPE_STACK = new LinkedList[0];

          /** Explicit referent used as SOURCE-PROCEDURE for a PUBLISH statement. */
          private Object publishSourceProcedure = null;

          /**
           * Stack of internal entry names.  Evolves together with the {@link #thisProceduresData} stack.
           */
          private ArrayDeque<String> internalEntryStack = new ArrayDeque<>();

          /** Resources pending to registered with THIS-PROCEDURE when the next block is entered. */
          private LinkedHashSet<WrappedResource> pendingResources = new LinkedHashSet<>();

          /** Flag indicating if static resources can be added to {@link #pendingResources}. */
          private boolean registerPendingResources = true;

          /** Map of relative names (i.e. used at the RUN statement) for each external program. */
          private IdentityHashMap<Object, String> relativeNames = new IdentityHashMap<>();

          /**
           * Indicates the current external program when a scope or finalizable notification occurs.
           */
          private Object processedProcedure = null;

          /** The resource currently being deleted. */
          private WrappedResource pendingResourceDelete = null;

          /** A stack containing the info about the next called program or internal entry. */
          private ArrayDeque<CalleeInfoImpl> calleeInfo = new ArrayDeque<>();

          /** The 4GL program name being currently initiating. */
          private String initiatingExtProg = null;

          /** The Java class for the current external program or OE class being executed. */
          private Class<?> initiatingExtProgClass = null;

          /** Map with flags indicating if this procedure should be deleted on exit. */
          private IdentityHashMap<Object, Boolean> deleteOnExit = new IdentityHashMap<>();

          /** A registry of persistent procedures which were instantiated by a remote side. */
          private Set<Object> remotePersistentProcedures = Collections.newSetFromMap(new IdentityHashMap<>());

          /** The global list of subscriptions, for this context. */
          private LinkedList<Subscription> subscriptions = new LinkedList<>();

          /** Per-event subscribes, to avoid iterating all subscriptions when performing a PUBLISH. */
          private HashMap<character, LinkedList<Subscription>> subscriptionsByEvent = new HashMap<>();

          /**
           * This will store the a snapshot of the block stack when the first global buffer was
           * used. This is used when a persistent procedure should be deleted: the delete will
           * be postponed at a block on this stack.
           */
          private IdentityHashMap<Object, Stack<BlockDefinition> > persistentProcStacks = new IdentityHashMap<>();

          /**
           * A set of persistent procedures which were deleted but are in a state that allow them to be deleted again
           */
          public Set<Object> staleProcedures = Collections.newSetFromMap(new IdentityHashMap<>());

          /**
           * Initialize this instance - read the <code>default-databases</code> configuration.
           */
          @Override
          public void init()
          {
             // load the per-package default databases
             DirectoryService ds = DirectoryService.getInstance();
             if (ds != null && ds.bind())
             {
                String path = Utils.findDirectoryNodePath(ds, "default-databases", null,
                                                          DirScope.SERVER, null);
                if (path != null)
                {
                   defaultDbs = new HashMap<>();
                   String[] pkgs = ds.enumerateNodes(path);
                   for (String pkg : pkgs)
                   {
                      String[] dbs = ds.getNodeStrings(path + "/" + pkg, "values");
                      defaultDbs.put(pkg, new HashSet<>(Arrays.asList(dbs)));
                   }
                }
                ds.unbind();
             }
          }

          /**
           * Adds a block stack snapshot to the persistent procedure stacks. This is should be called
           * when the stack shows the first time a global buffer in the referent is defined.
           *
           * @param    referent
           *           The persistent procedure to which the stack is mapped.
           * @param    stack
           *           The stack which should represent the current block stack snapshot.
           */
          public void addPersistentProcStack(Object referent, Stack<BlockDefinition> stack)
          {
             persistentProcStacks.put(referent, stack);
          }

          /**
           * Check if a referent is persistent and has a stack attached. If it has, it means that
           * it opened a global static buffer at least once.
           *
           * @param     referent
           *            The persistent procedure for which the checking is done.
           *
           * @return    {@code true} if the referent has a persistent procedure stack attached.
           */
          public boolean hasPersistentProcStack(Object referent)
          {
             return persistentProcStacks.containsKey(referent);
          }

          /**
           * Obtains the stack snapshot of one persistent procedure. The stack represent the block
           * stack when the first global buffer was defined inside the referent.
           *
           * @param    referent
           *           The persistent procedure for which the stack should be retrieved.
           *
           * @return   The block stack snapshot stored for the specified referent.
           *
           */
          public Stack<BlockDefinition> getPersistentProcStack(Object referent)
          {
             return persistentProcStacks.get(referent);
          }

          /**
           * Marks the specified referent as stale.
           *
           * @param     referent
           *            The referent which should be marked as stale.
           * @return    {@code true} if the provided referent was made stale for the first time.
           */
          public boolean addStaleReferent(Object referent)
          {
             if (persistentProcStacks.containsKey(referent) && !staleProcedures.contains(referent))
             {
                staleProcedures.add(referent);
                return true;
             }
             return false;
          }

          /**
           * Removes the referent information from the persistent procedure stack mapping.
           * This should be done only when the referent is deleted (never be used again).
           *
           * @param    referent
           *           The persistent procedure for which the entry should be removed.
           */
          public void removeStaleProcedure(Object referent)
          {
             persistentProcStacks.remove(referent);
             staleProcedures.remove(referent);
          }

          /**
           * Walk the {@link WorkArea#thisProceduresData THIS-PROCEDURE} stack and apply the search function;
           * return the first value given by the search function, once it returns a non-null value.
           *
           * @param    search
           *           The search function.
           *
           * @return   The first non-null value returned by the search function.
           */
          public Object searchInStack(Function<Object, Object> search)
          {
             Map<Object, Object> searched = new IdentityHashMap<>();

             Iterator<ProcedureData> iter = thisProceduresData.iterator();
             while (iter.hasNext())
             {
                Object referent = iter.next().getReferent();
                if (searched.containsKey(referent))
                {
                   continue;
                }

                Object ret = search.apply(referent);

                if (ret != null)
                {
                   return ret;
                }

                searched.put(referent, referent);
             }

             return null;
          }

          /**
           * Set the name of the 4GL program currently being instantiated, into the
           * {@link WorkArea#initiatingExtProg} field.
           *
           * @param    name
           *           The 4GL external program name.
           */
          public void setInstantingExternalProgram(String name)
          {
             initiatingExtProg = name;
          }

          /**
           * Set the Java class of the currently instantiating external program or OE class.
           *
           * @param    cls
           *           The Java class for the currently executing code.
           */
          public void setInstantingExternalProgramClass(Class<?> cls)
          {
             initiatingExtProgClass = cls;
          }

          /**
           * Check if an external program was ran persistent by a remote side.
           *
           * @param   referent
           *          The external program instance.
           *
           * @return  <code>true</code> if the referent is part of the
           *          {@link WorkArea#remotePersistentProcedures} map.
           */
          public boolean isRemotePersistentProcedure(Object referent)
          {
             return remotePersistentProcedures.contains(referent);
          }

          /**
           * Implementation for the PROGRAM-NAME functions. Resolves the legacy top level block (external
           * program, function, internal procedure or trigger), at the given depth. The index starts with
           * 0 for the currently executing top-level block.
           *
           * @param    idx
           *           The stack entry index.
           *
           * @return   The name of the stack entry or {@code null} if outside of stack.
           */
          public String getStackEntry(int idx)
          {
             if (idx < 0 || idx >= thisProceduresData.size()) // quick-out
             {
                return null;
             }

             Iterator<ProcedureData> procItr = thisProceduresData.iterator();
             Iterator<String> ieItr = internalEntryStack.iterator();

             Object referent = null;
             String iename = null;

             for (int i = 0; i <= idx;)
             {
                if (!procItr.hasNext())
                {
                   return null;
                }

                referent = procItr.next().getReferent();
                iename = ieItr.next();
                // do not include PERSISTENT_TRIGGER_PROCEDURE or THIS_PROCEDURE_OVERRIDE in the legacy
                // stack trace
                if (!(PERSISTENT_TRIGGER_PROCEDURE.equals(iename) ||
                      THIS_PROCEDURE_OVERRIDE.equals(iename)))
                {
                   i++;
                }
             }

             String name = relativeNames.get(referent);

             return EXTERNAL_PROGRAM.equals(iename) ? name : (iename + " " + name);
          }

          /**
           * Handle scope notifications, to maintain proper source, target, and
           * this-procedure references.
           *
           * @param    block
           *           The explicit block definition which required this notification.
           */
          @Override
          public void scopeStart(BlockDefinition block)
          {
             // WARNING: in TransactionManager, this is called only for top-level blocks.

             BlockType bt = block.type;

             if (block.topLevel)
             {
                // to minimize object creation but still add a scope in the stack, we use a marker at this moment.
                // The 'immutable' EMPTY_SCOPE_STACK marker will be replaced with a true container when the 1st
                // handler will be registered
                cleanupHandlers.push(EMPTY_SCOPE_STACK);
             }

             if (bt.equals(BlockType.EXTERNAL_PROC) ||
                 bt.equals(BlockType.INTERNAL_PROC) ||
                 bt.equals(BlockType.FUNCTION)      ||
                 bt.equals(BlockType.TRIGGER)       ||
                 bt.equals(BlockType.DATABASETRIGGER))
             {
                // always push info before pushing new THIS-PROCEDURE
                CalleeInfoImpl info = calleeInfo.peek();
                info.push();

                ProcedureData pdata = pMap.get(info.destination);

                // setting INSTANTIATING-PROCEDURE for external procedure block can be done only
                // after ProcedureData is created
                if (bt.equals(BlockType.EXTERNAL_PROC) && !tm.isRootNestingLevel())
                {
                   // if this is not the entry point, set the INSTANTIATING-PROCEDURE as the current
                   // THIS-PROCEDURE
                   pdata.instProc = new WeakReference<>(_thisProcedure(this));
                }

                pdata.thisCount = pdata.thisCount + 1;
                thisProceduresData.push(pdata);

                if (pdata.persistent && bt != BlockType.EXTERNAL_PROC && wp.hasUnnamedPool(pdata.referent))
                {
                   wp.scopeStart(block);
                }

                if (pendingResources.isEmpty())
                {
                   pdata.staticResources.push(Collections.emptySet());
                }
                else
                {
                   // collect the static resources in the correct scope
                   pdata.staticResources.push(new LinkedHashSet<>());

                   // process the newly created resources
                   for (WrappedResource res : pendingResources)
                   {
                      if (res instanceof HandleResource)
                      {
                         ((HandleResource) res).setInstantiatingProcedure(pdata.phandle);
                      }
                   }

                   Set<WrappedResource> res = pdata.staticResources.peek();
                   res.addAll(pendingResources);
                   pendingResources.clear();
                }
             }
          }

          /**
           * Handle scope notifications, to maintain proper source, target, and
           * this-procedure references.
           */
          @Override
          public void scopeFinished()
          {
             // WARNING: in TransactionManager, this is called only for top-level blocks.

             BlockDefinition block = tm.getBlock();
             BlockType bt = block.type;

             if (tm.isTopLevelBlock())
             {
                // execute the cleanup handlers
                List<Runnable>[] handlers = cleanupHandlers.pop();
                if (handlers != EMPTY_SCOPE_STACK)
                {
                   for (int i = 0; i < handlers.length; i++)
                   {
                      List<Runnable> handlerSet = handlers[i];
                      if (handlerSet != null)
                      {
                         for (Runnable handler : handlerSet)
                         {
                            handler.run();
                         }
                      }
                   }
                }
             }

             if (bt.equals(BlockType.EXTERNAL_PROC) ||
                 bt.equals(BlockType.INTERNAL_PROC) ||
                 bt.equals(BlockType.FUNCTION)      ||
                 bt.equals(BlockType.TRIGGER)       ||
                 bt.equals(BlockType.DATABASETRIGGER))
             {
                if (tm.isRootNestingLevel() && TransactionImpl.isOpenByTxInitProc())
                {
                   handle txInitProc = TransactionImpl.get().getTxInitProcedure();
                   if (txInitProc._isValid())
                   {
                      ProcedureData pd = locateProcedure(this, txInitProc.get());
                      TransactionImpl.requestFinished(pd.transactionModeAutoChained, true);
                   }
                }

                // pop this-procedure from the stack and cleanup
                ProcedureData pdata = thisProceduresData.pop();
                pdata.thisCount = pdata.thisCount - 1;
                Object referent = pdata.getReferent();

                boolean extProc = bt == BlockType.EXTERNAL_PROC;

                if (extProc)
                {
                   // mark the external procedure as having finished executing
                   pdata.executed = true;
                }

                CalleeInfoImpl info = calleeInfo.peek();
                info.pop();

                boolean persistent = pdata.persistent;

                // check if we have a postponed delete
                Boolean delete = deleteOnExit.get(referent);

                if (delete != null && delete)
                {
                   // check if this is the last usage of the referent

                   if (pdata.thisCount > 0)
                   {
                      // still used, wait for the last proc on the stack to finish.
                      delete = null;
                   }
                   else
                   {
                      // this is no longer a persistent procedure being used, can be deleted

                      deleteOnExit.remove(referent);

                      persistentProcedures.remove(referent);
                      persistent = false;
                   }
                }

                if (block.widgetPool)
                {
                   wp.scopeFinished(referent, extProc, persistent);
                }

                boolean deleted = false;

                // notify the widget-pool management a procedure has finished; this needs to be done
                // here to ensure that the correct THIS-PROCEDURE handle is available
                if (!(extProc && persistent))
                {
                   // bypass this if there was a postponed delete, as the resources were deleted by
                   // the delete(referent) call; otherwise delete this external, non-persistent,
                   // procedure
                   if (extProc && !persistent)
                   {
                      deleted = delete(this, referent, true);

                      // already deleted, nothing else to do
                      delete = null;
                   }
                   else
                   {
                      // delete the static resources created in an internal proc/function/trigger
                      deleteResources(this, pdata.staticResources.pop());
                   }
                }
                else
                {
                   cleanup(referent);
                }

                if (delete != null && delete)
                {
                   String name = getAbsoluteName(referent);

                   // force the delete of this external procedure, as some internal proc/func/trig
                   // has called DELETE OBJECT THIS-PROCEDURE

                   Object saved = tm.getNowIterating();
                   tm.setNowIterating(null);
                   try
                   {
                      // this is the same approach as in ControlFlowOps.executeDestructor
                      deleted = delete(this, referent, true);
                   }
                   finally
                   {
                      tm.setNowIterating(saved);
                   }

                   if (LOG.isLoggable(Level.FINE))
                   {
                      LOG.log(Level.FINE, "Processing postponed delete for procedure " + name +
                                          " has " + (deleted ? "passed" : "failed"));
                   }
                }

                if (deleted)
                {
                   handle.removeResource(new ExternalProgramWrapper(referent));
                }
             }
          }

          /**
           * Provides a notification that an external scope is about to be deleted.
           * <p>
           * This implementation is a no-op.
           */
          @Override
          public void scopeDeleted()
          {
             // no-op
          }

          /**
           * Get the {@link ScopeId} for the instance.
           *
           * @return   <code>null</code>, as the scope notification is done explicitly via
           *           {@link TransactionManager}.
           */
          @Override
          public ScopeId getScopeId()
          {
             // no ID required, as this is called explicitly in TransactionManager
             return null;
          }

          /**
           * Get the map of session's super-procedures.
           */
          public Map<Object, ProcedureData> getSessionSuperProcedures()
          {
             return getSuperProcedures(null, true);
          }

          /**
           * Get the map of the super-procedures for the given referent.
           *
           * @param    referent
           *           The external program instance for which the super-procedures
           *           are needed.
           * @param    readOnly
           *           Flag indicating that the result will not be altered.
           */
          public Map<Object, ProcedureData> getSuperProcedures(Object referent, boolean readOnly)
          {
             if (referent == null)
             {
                return sessionSuperProcs;
             }

             ProcedureData pdata = pMap.get(referent);
             if (!readOnly && pdata.superProcedures == Collections.EMPTY_MAP)
             {
                pdata.superProcedures = new LinkedHashMap<>();
             }
             return pdata.superProcedures;
          }

          /**
           * Attempt cleanup of the given external program instance. If is no
           * longer used as a persistent procedure, super procedure, or as a
           * system handle, the referent will be removed from the {@link #pMap}
           * map.
           *
           * @param    referent
           *           The external program instance for which needs to be cleaned
           *           up.
           */
          public void cleanup(Object referent)
          {
             ProcedureData pd = pMap.get(referent);
             // if no longer used, remove from global map
             if (pd.thisCount == 0                                    &&
                 pd.targetCount == 0                                  &&
                 pd.sourceCount == 0                                  &&
                 !persistentProcedures.contains(referent)             &&
                 !sessionSuperProcs.containsKey(referent)             &&
                 !superProcForExtProc(referent, persistentProcedures) &&
                 !superProcForExtProc(referent, sessionSuperProcs.keySet()))
             {
                forceCleanup(referent);
             }
          }

          /**
           * Cleanup of the given external program instance. The referent will be
           * removed from the {@link #pMap} map.
           *
           * @param    referent
           *           The external program instance for which needs to be cleaned
           *           up.
           */
          public void forceCleanup(Object referent)
          {
             // remove from global map
             ProcedureData pd = pMap.remove(referent);
             relativeNames.remove(referent);

             ProcedureManager.cleanupSubscriptions(referent);
          }

          /**
           * Get the Java class associated with the currently executing code.
           *
           * @return   See above.
           */
          private Class<?> getExecuting()
          {
             Class<?> def = calleeInfo.peek().executing;
             if (def == null)
             {
                Object thisProc = thisProceduresData.peek().getReferent();
                if (thisProc.getClass() == Object.class)
                {
                   def = ObjectOps.getStaticClass(thisProc);
                }
                else
                {
                   def = thisProc.getClass();
                }
             }

             return def;
          }

          /**
           * Get an iterator over the stack of currently executing code.
           *
           * @return    An iterator of Java classes associated with the code executing on the stack.
           */
          private Iterator<Class<?>> executingIterator()
          {
             Iterator<CalleeInfoImpl> iter = calleeInfo.iterator();

             return new Iterator<Class<?>>()
             {
                @Override
                public boolean hasNext()
                {
                   return iter.hasNext();
                }

                @Override
                public Class<?> next()
                {
                   return iter.next().executing;
                }
             };
          }

          /**
           * Get an iterator over the stack of destination resolved code.
           *
           * @return    An iterator of Java classes associated with the code on the stack.
           */
          private Iterator<Object> destinationIterator()
          {
             Iterator<CalleeInfoImpl> iter = calleeInfo.iterator();

             return new Iterator<Object>()
             {
                @Override
                public boolean hasNext()
                {
                   return iter.hasNext();
                }

                @Override
                public Object next()
                {
                   return iter.next().getDestination();
                }
             };
          }

          /**
           * Check if the given referent is a super-procedure of one of the
           * procedures in the given list.
           *
           * @param    referent
           *           The external program instance.
           * @param    progs
           *           The list of external programs for which their
           *           super-procedures list will be searched for the referent.
           *
           * @return   <code>true</code> if it was found as a super-procedure for
           *           one of the given procedures.
           */
          private boolean superProcForExtProc(Object referent, Collection<Object> progs)
          {
             for (Object prog : progs)
             {
                ProcedureData pp = pMap.get(prog);
                if (pp.superProcedures.isEmpty())
                {
                   continue;
                }

                for (Object sreferent : pp.superProcedures.keySet())
                {
                   if (sreferent == referent)
                   {
                      return true;
                   }
                }
             }

             return false;
          }
       }

       /**
        * Contains data about a subscription.
        */
       private static abstract class Subscription<T>
       {
          /** The client session which registered this subscription. */
          private final Session session = SessionManager.get().getSession();

          /** The subscriber. */
          protected final T subscriber;

          /** The subscribed event. */
          protected final character event = new character();

          /** The publisher to match. <code>null</code> represents any source. */
          protected final Object publisher;

          /** Flag indicating that this subscription can't be removed. */
          protected boolean readOnly = false;

          /** When specified, it references exactly what needs to be invoked. */
          protected InternalEntry signature = null;

          /**
           * Build a new subscription with the specified data.
           *
           * @param    subscriber
           *           The subscriber.
           * @param    event
           *           The event to subscribe.
           * @param    publisher
           *           The event's publisher. May be <code>null</code>.
           */
          public Subscription(T subscriber, character event, Object publisher)
          {
             this.subscriber = subscriber;
             this.publisher = publisher;
             this.event.assign(event);
          }

          /**
           * Execute a PUBLISH request, by posting the event to the actual {@link #session context}.
           *
           * @param    modes
           *           The parameter modes.
           * @param    params
           *           The PUBLISH parameters.
           */
          public abstract void publishRemote(String modes, Object[] params);

          /**
           * Execute a PUBLISH request, in this context.
           *
           * @param    publisher
           *           The publisher which posted the PUBLISH event.
           * @param    errors
           *           A list to collect any errors during PUBLISH.
           * @param    modes
           *           The parameter modes.
           * @param    params
           *           The PUBLISH parameters.
           */
          public abstract void publish(Object       publisher,
                                       List<String> errors,
                                       String       modes,
                                       Object[]     params);
       }

       /**
        * Contains data about a subscription from an {@link ExternalResource}.
        */
       private static class ExternalSubscription
       extends Subscription<ExternalResource>
       {
          /**
           * Build a new subscription with the specified data.
           *
           * @param    subscriber
           *           The subscriber.
           * @param    event
           *           The event to subscribe.
           * @param    publisher
           *           The event's publisher. May be <code>null</code>.
           */
          public ExternalSubscription(ExternalResource subscriber, character event, Object publisher)
          {
             super(subscriber, event, publisher);
          }

          /**
           * Execute a PUBLISH request, by posting the event to the actual {@link #session context}.
           *
           * @param    modes
           *           The parameter modes.
           * @param    params
           *           The PUBLISH parameters.
           */
          @Override
          public void publishRemote(String modes, Object[] params)
          {
             publishWorker(params, true);
          }

          /**
           * Execute a PUBLISH request, in this context.
           *
           * @param    publisher
           *           The publisher which posted the PUBLISH event.
           * @param    errors
           *           A list to collect any errors during PUBLISH.
           * @param    modes
           *           The parameter modes.
           * @param    params
           *           The PUBLISH parameters.
           */
          @Override
          public void publish(Object publisher, List<String> errors, String modes, Object[] params)
          {
             publishWorker(params, false);
          }

          /**
           * Worker to perform PUBLISH to a remote side.
           *
           * @param    params
           *           The PUBLISH parameters.
           * @param    delayed
           *           Flag indicating if the PUBLISH is executed immediately or to be posted to the
           *           client's event queue, when <code>false</code>.
           */
          private void publishWorker(Object[] params, boolean delayed)
          {
             // stringify all params...
             String[] sparams = new String[params.length];
             for (int i = 0; i < params.length; i++)
             {
                Object p = params[i];

                if (p instanceof BaseDataType)
                {
                   sparams[i] = ((BaseDataType) p).toStringMessage();
                }
                else
                {
                   sparams[i] = (p == null ? null : p.toString());
                }
             }

             LogicalTerminal.publish(this.subscriber, this.event.toStringMessage(), sparams, delayed);
          }
       }

       /**
        * Contains data about a legacy subscription made by a certain procedure.
        */
       private static class LegacySubscription
       extends Subscription<Object>
       {
          /** The method to be invoked. May be <code>null</code>. */
          private final String method;

          /**
           * Build a new subscription with the specified data.
           *
           * @param    subscriber
           *           The subscriber procedure.
           * @param    event
           *           The event to subscribe.
           * @param    publisher
           *           The event's publisher. May be <code>null</code>.
           * @param    method
           *           The method to be invoked. When <code>null</code>, the event name will be used
           *           as the method name.
           */
          public LegacySubscription(Object subscriber, character event, Object publisher, character method)
          {
             super(subscriber, event, publisher);
             this.method = method == null ? event.getValue() : method.getValue();
          }

          /**
           * Execute a PUBLISH request, by posting the event to the actual {@link #session context}.
           *
           * @param    modes
           *           The parameter modes.
           * @param    params
           *           The PUBLISH parameters.
           */
          @Override
          public void publishRemote(String modes, Object[] params)
          {
             ServerEvent evt = new ServerEvent(-1,
                                               "PUBLISH",
                                               () -> publish(null, new ArrayList<>(), modes, params));

             LogicalTerminal.postServerEvent(evt);
          }

          /**
           * Execute a PUBLISH request, in this context.
           *
           * @param    publisher
           *           The publisher which posted the PUBLISH event.
           * @param    errors
           *           A list to collect any errors during PUBLISH.
           * @param    modes
           *           The parameter modes.
           * @param    params
           *           The PUBLISH parameters.
           */
          @Override
          public void publish(Object publisher, List<String> errors, String modes, Object[] params)
          {
             WorkArea wa = work.obtain();

             ProcedureData pd = wa.pMap.get(this.subscriber);

             // business logic can unsubscribe during this loop
             if (pd == null)
             {
                return;
             }

             String relName = getRelativeName(wa, pd.phandle.get());

             // invoke the associated procedure. there are some rules here:
             // - changes of INPUT-OUTPUT parameters are inherited from one call to another
             // - OUTPUT parameters are always overridden
             String iename = (this.method == null ? event.toStringMessage() : this.method);

             if (signature != null)
             {
                if (ObjectOps.isLegacyObject(subscriber))
                {
                   ControlFlowOps.invokeLegacyMethod(signature, true, subscriber,
                                                     relName, iename,
                                                     modes, params);
                }

                return;
             }

             // check if current procedure (or its SUPER) has a definition for target
             handle phandle = ControlFlowOps.resolveProgramForProcedure(pd.phandle, iename);

             if (phandle == null)
             {
                final String msg =
                   "PUBLISH statement failed. Procedure %s has no entry point for %s";

                // postpone error reporting until all the invocations are done,
                // we want to prevent setting of deferred error as this would
                // produce an invalid handle in the call to handle.ctor below
                errors.add(String.format(msg, relName, iename));
                return;
             }

             // for the duration of this call, the SOURCE-PROCEDURE must be assumed as the
             // publisher.
             // the received publisher argument may be null, if the PUBLISH originates from a remote
             // side.  in this case, THIS-PROCEDURE will remain unchanged.
             wa.publishSourceProcedure = publisher;

             // backup the parameters
             Object[] saveParams = params == null || modes == null ? null : new Object[params.length];
             if (saveParams != null)
             {
                for (int i = 0; i < params.length; i++)
                {
                   saveParams[i] = params[i];
                }
             }

             try
             {
                // even if silent error mode is enabled by default, when the procedure is
                // executed it will be disabled for the duration of the execution and re-enabled
                // when the procedure ends. TransactionManager.push/popScope takes care of this.
                ControlFlowOps.invokeInWithMode(iename, pd.phandle, modes, params);
             }
             finally
             {
                // ensure it remains null, in case for any reason the internal proc is not
                // executed
                wa.publishSourceProcedure = null;

                if (saveParams != null)
                {
                   for (int i = 0; i < saveParams.length; i++)
                   {
                      if (saveParams[i] != params[i]            &&
                          saveParams[i] instanceof BaseDataType &&
                          modes.charAt(i) != 'I')
                      {
                         ((BaseDataType) saveParams[i]).assign(((BaseDataType) params[i]));
                         params[i] = saveParams[i];
                      }
                   }
                }
             }
          }
       }

       /**
        * A container with various data about the next called program/internal-entry, needed by
        * {@link WorkArea#scopeStart}.
        */
       private static class CalleeInfoImpl
       implements CalleeInfo
       {
          /** Flag indicating if the destination is in a super-procedure. */
          private final boolean superCall;

          /** The target procedure, which was targeted by the caller. */
          private final Object target;

          /** The procedure where the internal entry was resolved. */
          private final Object destination;

          /**
           * The internal entry name. In case of triggers or external programs, use the
           * {@link #EXTERNAL_PROGRAM} and {@link #TRIGGER_PROCEDURE} constants.
           */
          private final String iename;

          /** The currently executing OE class or external program. */
          private final Class<?> executing;

          /**
           * The external program name; will be non-null only when we are invoking an external
           * program.
           */
          private final String pname;

          /** Flag indicating we are calling a user-defined function. */
          private final boolean function;

          /** Flag indicating a dynamic call. */
          private final boolean dynamicCall;

          /** Flag indicating we are calling a method. */
          private final boolean method;

          /** Flag indicating the external program was ran persistent. */
          private final boolean persistentProc;

          /** Flag indicating the external program was ran as singleton. */
          private final boolean singletonProc;

          /** Flag indicating the external program was ran as single-run. */
          private final boolean singleRunProc;

          /** Access to context-local data. */
          private final WorkArea wa;

          /** The already known legacy name for the internal entry. */
          private String legacyName;

          /**
           * Create a new instance.
           *
           * @param    wa
           *           Access to context-local data.
           * @param    def
           *           The currently executing OE class or external program.
           * @param    superCall
           *           Flag indicating if the destination is in a super-procedure.
           * @param    target
           *           The target procedure, which was targeted by the caller.
           * @param    destination
           *           The procedure where the internal entry was resolved.
           * @param    iename
           *           The internal entry name. In case of triggers or external programs, use the
           *           {@link #EXTERNAL_PROGRAM} and {@link #TRIGGER_PROCEDURE} constants.
           * @param    pname
           *           The external program name; will be non-null only when we are invoking an external
           *           program.
           * @param    function
           *           Flag indicating we are calling a user-defined function.
           * @param    persistentProc
           *           Flag indicating the external program was ran persistent.
           * @param    singletonProc
           *           Flag indicating the external program was ran singleton.
           * @param    singleRunProc
           *           Flag indicating the external program was ran single-run.
           * @param    dynamicCall
           *           Flag indicating a dynamic call
           */
          public CalleeInfoImpl(WorkArea wa,
                                Class<?> def,
                                boolean superCall,
                                Object  target,
                                Object  destination,
                                String  iename,
                                String  pname,
                                boolean function,
                                boolean persistentProc,
                                boolean singletonProc,
                                boolean singleRunProc,
                                boolean dynamicCall)
          {
             this.wa              = wa;
             this.executing       = def;
             this.superCall       = superCall;
             this.target          = target;
             this.destination     = destination;
             this.iename          = iename;
             this.pname           = pname;
             this.function        = function;
             this.method          = (function && ObjectOps.isLegacyObject(destination));
             this.persistentProc  = persistentProc;
             this.singletonProc   = singletonProc;
             this.singleRunProc   = singleRunProc;
             this.dynamicCall     = dynamicCall;
          }

          /**
           * Set the {@link #legacyName} so that {@link ProcedureManager#pushInternalEntry} will avoid resolve
           * it via {@link SourceNameMapper}, as that is expensive.  This is done only in cases where the
           * internal entry's legacy name is previously resolved.
           *
           * @param    legacyName
           *           The resolved internal entry's legacy name.
           */
          @Override
          public void setLegacyName(String legacyName)
          {
             this.legacyName = legacyName;
          }

          /**
           * Called by {@link WorkArea#scopeStart} - depending on the callee's configuration, it
           * populates the procedure system handles and maintains persistent procedures.
           */
          public void push()
          {
             if (pname != null && !wa.pMap.containsKey(target))
             {
                // this is an external procedure invocation...
                handle htarget = new handle(new ExternalProgramWrapper(target));
                ProcedureManager.addProcedure(wa, htarget, pname,
                                              persistentProc, singletonProc, singleRunProc, false);
             }

             if (!superCall)
             {
                if (!wa.thisProceduresData.isEmpty())
                {
                   ProcedureData pd;
                   if (wa.publishSourceProcedure != null)
                   {
                      pd = wa.pMap.get(wa.publishSourceProcedure);
                      wa.publishSourceProcedure = null;
                   }
                   else
                   {
                      pd = wa.thisProceduresData.peek();
                   }
                   pd.sourceCount = pd.sourceCount + 1;
                   wa.sourceProceduresData.push(pd);
                }

                ProcedureData pd = wa.pMap.get(target);
                pd.targetCount = pd.targetCount + 1;
                wa.targetProceduresData.push(pd);
             }

             pushInternalEntry(wa, destination, iename, function, legacyName);
          }

          /**
           * Called by {@link WorkArea#scopeFinished()} - depending on the callee's configuration, it
           * backs out the data pushed by {@link #push()}, so the state will be the same as before
           * the callee as invoked.
           */
          public void pop()
          {
             wa.internalEntryStack.pop();

             if (!superCall)
             {
                if (!wa.thisProceduresData.isEmpty())
                {
                   ProcedureData pd = wa.sourceProceduresData.pop();
                   pd.sourceCount = pd.sourceCount - 1;
                }

                ProcedureData pd = wa.targetProceduresData.pop();
                pd.targetCount = pd.targetCount - 1;
             }
          }

          /**
           * The internal entry name. In case of triggers or external programs, returns the
           * {@link ProcedureManager#TRIGGER_PROCEDURE} and {@link ProcedureManager#EXTERNAL_PROGRAM}
           * constants respectively.
           *
           * @return   The internal entry name.
           */
          @Override
          public String getInternalEntryName()
          {
             return iename;
          }

          /**
           * The procedure where the internal entry was resolved.
           *
           * @return   A valid instance.
           */
          @Override
          public Object getDestination()
          {
             return destination;
          }

          /**
           * The external program name; will be non-null only when we are invoking an external
           * program.
           *
           * @return   A {@link String} representing the program name.
           */
          @Override
          public String getExternalProgramName()
          {
             return pname;
          }

          /**
           * Flag indicating a user-defined function is called.
           *
           * @return   A boolean flag.
           */
          @Override
          public boolean isFunction()
          {
             return function;
          }

          /**
           * Flag indicating a method is called.
           *
           * @return   A boolean flag.
           */
          @Override
          public boolean isMethod()
          {
             return method;
          }

          /**
           * Flag indicating the call is dynamic.
           *
           * @return   A boolean flag.
           */
          @Override
          public boolean isDynamicCall()
          {
             return dynamicCall;
          }
       }

       /**
        * Container for various procedure data.
        */
       static class ProcedureData
       {
          /** The set of implicitly connected databases for this procedure. */
          private Set<String> connectedDbs = null;

          /**
           * Flag indicating that a tx needs to be automatically opened after any pending rollback
           * or commit (the TRANSACTION-MODE AUTOMATIC CHAINED language statement is in effect
           * for this procedure.
           */
          private boolean transactionModeAutoChained = false;

          /** The list of super-procedures for this procedure, by external program instance. s*/
          private Map<Object, ProcedureData> superProcedures = Collections.emptyMap();

          /**
           * The map of name of functions defined IN handle in this external program, to their
           * {@link Resolvable} instance.
           */
          private Map<String, Resolvable> inHandleFunctions = Collections.emptyMap();

          /**
           * A handle instance for this procedure, so that other handles can be instantiated from it.
           */
          private final handle phandle;

          /** The actual referent for this procedure. */
          private final Object referent;

          /** The current search-mode for this external program. */
          private SearchMode searchMode = null;

          /** The private data of this procedure handle. */
          private String privateData = null;

          /** Implementation of ADM-DATA attribute. */
          private String admData = null;

          /** Implementation of UNIQUE-ID attribute. */
          private Long uniqueID = null;

          /** Handle referencing the instantiating procedure. */
          private WeakReference<Object> instProc = null;

          /**
           * Set of {@link Scopeable} instances which need notification when the persistent procedure
           * gets deleted.
           */
          private Set<Scopeable> scopeables = Collections.emptySet();

          /**
           * Set of {@link Finalizable} instances which need notification when the persistent procedure
           * gets deleted.
           */
          private Set<Finalizable> finalizables = Collections.emptySet();

          /**
           * A stack holding the resources created in each top-level block.
           */
          private Deque<Set<WrappedResource>> staticResources = new ArrayDeque<>();

          /**
           * A value of CURRENT-WINDOW attribute of THIS-PROCEDURE handle
           */
          private WrappedResource currentWindow = null;

          /** A set of all created (and still alive) resources, in this procedure (dynamic or not). */
          private Map<HandleResource, Object> createdResources = new WeakHashMap<>();

          /** Flag indicating this procedure has ROUTINE-LEVEL UNDO, THROW. */
          private boolean routineLevelUndoThrow = false;

          /** Flag indicating this procedure has BLOCK-LEVEL UNDO, THROW. */
          private boolean blockLevelUndoThrow = false;

          /** Flag indicating if the program was ran PERSISTENT. */
          private boolean persistent;

          /** Flag indicating if the program was ran SINGLE-RUN. */
          private boolean singleRun;

          /** Flag indicating if the program was ran SINGLETON. */
          private boolean singleton;

          /** Flag indicating that the program has finished executing. */
          private boolean executed = false;

          /** Flag indicating if this procedure was added as a session super-procedure. */
          private boolean isSessionSuperProcedure = false;

          /** The set of procedures to which it was added as a super-procedure. */
          private Set<Object> addedToSuperProcedures = Collections.emptySet();

          /** The name of this program or class. */
          private final String name;

          /** Counts the number of times this {@link #referent} appears on current stack as SOURCE-PROCEDURE. */
          private int sourceCount = 0;

          /** Counts the number of times this {@link #referent} appears on current stack as THIS-PROCEDURE. */
          private int thisCount = 0;

          /** Counts the number of times this {@link #referent} appears on current stack as TARGET-PROCEDURE. */
          private int targetCount = 0;

          /**
           * Create a new {@link ProcedureData} instance using the given
           * procedure handle and search-mode.
           *
           * @param    phandle
           *           The procedure handle.
           * @param    searchMode
           *           The search-mode. May be null.
           * @param    name
           *           The name if this program or class.
           */
          ProcedureData(handle phandle, SearchMode searchMode, String name)
          {
             this.phandle = new handle(phandle);
             this.referent = this.phandle.get();
             this.searchMode = searchMode;
             this.name = name;
          }

          /**
           * Get the {@link #persistent} flag.
           *
           * @return   See above.
           */
          boolean isPersistent()
          {
             return persistent;
          }

          /**
           * Get the {@link #referent}.
           *
           * @return   See above.
           */
          Object getReferent()
          {
             return referent;
          }

          /**
           * Check if the search-mode for this external program is SEARCH-SELF.
           *
           * @return   See above.
           */
          boolean isSearchSelf()
          {
             return searchMode == null || searchMode == SearchMode.SEARCH_SELF;
          }
       }

       /**
        * 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_5}.
           */
          @Override
          public WeightFactor getWeight()
          {
             return WeightFactor.LEVEL_5;
          }

          /**
           * Retrieve the {@link WorkArea} instance.  Cross-dependencies will be initialized only after the
           * context-local instance is set.
           *
           * @return   The {@link WorkArea} instance.
           */
          public WorkArea obtain()
          {
             WorkArea wa = get(false);
             if (wa == null)
             {
                wa = new WorkArea();
                wa.init();

                set(wa);

                wa.tm = TransactionManager.getTransactionHelper();
                wa.wp = WidgetPool.getHelper();
                wa.h  = handle.getHelper();
                wa.oh = ObjectOps.getObjectHelper();
                wa.errHelper = ErrorManager.getErrorHelper();
             }

             return wa;
          }

          /**
           * Initializes the work area, the first time it is requested within a
           * new context. Registers the {@link WorkArea} instance with the
           * {@link TransactionManager}, for scope notifications.
           *
           * @return   The newly instantiated work area.
           */
          protected synchronized WorkArea initialValue()
          {
             return new WorkArea();
          }
       }
    }