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();
      }
   }
}