WidgetPool.java

/*
** Module   : WidgetPool.java
** Abstract : widget pools management functions
**
** Copyright (c) 2010-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------------Description---------------------------------
** 001 SVL 20101004 Created initial version with no implementation.
** 002 CA  20130131 Moved to util package (as other non-widget resources can be pooled). Added 
**                  DELETE WIDGET-POOL support.
** 003 CA  20130918 Added runtime support for widget-pools.
** 004 CA  20140228 Delete only valid resources. Fixed merging of unnamed persistent pools to 
**                  previous scope.
** 005 OM  20141117 Fixed widget-pool management for persistent procedures.
** 006 CA  20150429 Fixed a mem-leak in deleteWidgetPools.
** 007 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 008 ECF 20171122 Minor optimizations.
** 009 CA  20190313 The persistent program's unnamed pools must be saved and pushed on the stack
**                  whenever one of its internal entries get executed.
**     CA  20190325 Implemented ProcedureHelper instead of direct usage of the static API.
**                  This allows the elimination of context local usage in ProcedureManager.
**     CA  20190326 Refactored to reduce the context-local usage.
** 010 CA  20190811 Optimized deleteNonPersistentPools, when there are no IDs in the set.
** 011 CA  20200505 Set the ProcedureManager and TransactionManager helper only for runtime.
** 012 CA  20201117 Fixed a leak with unnamedProcPools, which was not removing deleted referents.
**     CA  20210310 Small performance improvement when processing unnamed/named pools on scope finish (avoid  
**                  the 'add' overhead if they are empty). 
** 013 CA  20220120 Performance improvement for scope processing - don't build new collections until they are
**                  needed.
**     CA  20220201 A small performance improvement in scopeFinished.
**     CA  20220409 Fixed a bug when invoking a procedure in an external program: upon exit of that internal
**                  procedure, the external program's unnamed pools must survive (until it gets deleted).
**     CA  20221010 Performance improvements - avoid the ProcedureData lookup, by keeping a parallel stack of
**                  this data for THIS-PROCEDURE.  Refs #6826
**     CA  20230110 Keep a single 'ids' in WorkArea which will be used in 'scopeFinished', to avoid creating
**                  a new instance on each scope finished call.
**     CA  20230110 Moved some other APIs to WidgetPoolHelper, to reduce ContextLocal.get calls.
** 014 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 015 CA  20230724 Further reduce context-local usage.
** 016 CA  20230817 HandleOps.delete executing for a DELETE WIDGET-POOL is not considered an explicit DELETE 
**                  OBJECT (which would prevent deleting a temp-table/buffer part of a dataset).
** 017 CA  20231031 Added 'BlockDefinition' parameter to 'scopeStart'.
** 018 CA  20231216 Small performance improvement.
**     CA  20231221 Refactored the WidgetPool scopeable support to register for processing as needed.
** 019 CA  20231216 Fixed regression related to widget pools used in OO (the issue was with the implicit 
**                  widget pool for a class with USE-WIDGET-POOL, for which the instance gets deleted 
**                  implicitly when a block finishes). 
** 020 CA  20240105 Fixed regression in H019 - running an internal entry in a persistent procedure which 
**                  has an unnamed pool must push WidgetPool scope for this internal entry.
** 021 DDF 20240222 Avoid NPE since the head of the wa.unnamedPoolScopes might be null.
** 022 ES  20250109 Changed resources in WidgetPoolData from HashSet to LinkedHashSet.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import java.util.*;
import java.util.logging.*;

import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.ObjectOps.ObjectHelper;
import com.goldencode.p2j.util.ProcedureManager.ProcedureData;
import com.goldencode.p2j.util.logging.*;

/**
 * Widget-pool management class.
 * <p>
 * A widget pool is a a mechanism to explicitly define the scope at which resources get an 
 * "implicit" DELETE OBJECT when the control flow causes the program to exit that particular 
 * scope. At its most basic, a widget pool is just a list of resources that get deleted, an 
 * optional name, an optional persistent flag and an association with a block scope that exists at
 *  a "top-level" block (external proc, internal proc, user-defined function and trigger). 
 * A block scope will only open and close at top-level blocks (external program, procedure, 
 * function or trigger). Inner blocks don't cause any scoping behavior.
 * <p>
 * All pools are context-local, as they are private to the user's session.  No session-level 
 * widget-pool cleanup is implemented at this time. The current implementation of all the 
 * poolable resources does not require any session-level cleanup processing that is driven by 
 * widget pools. Should the implementation of any one of the poolable resources be modified to 
 * be dependent upon session-level widget-pool cleanup, then this feature will have to be added.
 * <p>
 * A pool will contain only resources which are dynamically created (and must be removed using
 * a DELETE statement), and only a sub-set of them can be pooled. From the list of CREATE 
 * statements, only the following statements accept a named pool:
 * <ul>
 * <li>CREATE BROWSE</li>
 * <li>CREATE BUFFER</li>
 * <li>CREATE CALL</li>
 * <li>CREATE DATASET</li>
 * <li>CREATE DATA-SOURCE</li>
 * <li>CREATE QUERY</li>
 * <li>CREATE SAX-ATTRIBUTES</li>
 * <li>CREATE SAX-READER</li>
 * <li>CREATE SAX-WRITER</li>
 * <li>CREATE SOAP-HEADER</li>
 * <li>CREATE SOAP-HEADER-ENTRYREF</li>
 * <li>CREATE TEMP-TABLE handle</li>
 * <li><pre>CREATE { BUTTON     | COMBO-BOX | CONTROL-FRAME | DIALOG-BOX     |
 *         EDITOR     | FILL-IN   | FRAME         | IMAGE          |
 *         MENU-ITEM  | RADIO-SET | RECTANGLE     | SELECTION-LIST |
 *         MENU       | SLIDER    | SUB-MENU      | TEXT           |
 *         TOGGLE-BOX | WINDOW    | VALUE ( string-expression )
 *       }</pre></li>
 * <li>CREATE X-DOCUMENT</li>
 * <li>CREATE X-NODEREF</li>
 * </ul>
 * Even 4GL has a default persistent unnamed pool associated with the user's session (and always 
 * stands on the bottom of the stack of unnamed pools), we do not implement this. For P2J, if 
 * there is no explicitly created unnamed pool, it means the unnamed persistent pool is in effect,
 * and we will not add the resources to it.  As 4GL doesn't offer any mechanism of accessing a 
 * pool's resources, the pool for a resource or the pool stack, plus the resources already exist
 * at most for the duration of the user's session, adding them to this pool will add unnecessary
 * overhead in P2J.  Thus, in P2J a resource may be:
 * <ul>
 * <li>unpooled (no pool will contain it), if no unnamed pool exists when the CREATE statement was
 *     executed, as there is no default unnamed pool. This is the equivalent of "part of the 
 *     default unnamed persistent pool" in 4GL.</li>
 * <li>part of only one pool (named if IN WIDGET-POOL clause is present or unnamed, if no 
 *     IN WIDGET-POOL clause is present and there is an unnamed pool).</li>
 * </ul>
 * <p>
 * The following rules describe how named and unnamed pools are implemented:
 * <p>
 * <h3>Unnamed Pools</h3>
 * Each created unnamed pool (non-persistent) is active for the current scope.  In the current 
 * scope, unnamed pools are "stacked".  A chain of CREATE WIDGET-POOL statements will create a set
 * of unnamed pools and they will be deleted in the reversed order of the creation, in LIFO order:
 * <pre>
 * 1: DEF VAR h1 AS HANDLE.
 * 2: DEF VAR h2 AS HANDLE.
 * 
 * 3: CREATE WIDGET-POOL. // U1
 * 4: CREATE BUTTON h1.
 * 
 * 5: CREATE WIDGET-POOL. // U2
 * 6: CREATE BUTTION h2.
 * 
 * 7: DELETE WIDGET-POOL. // U2
 * 8: DELETE WIDGET-POOL. // U1
 * </pre>
 * At step 3 and 5, an unnamed pool is created, identified by U1 and U2. At step 4, the resource
 * referenced by handle <code>h1</code> (R1) is added to unnamed pool U1 and at step 6, the 
 * resource referenced by handle <code>h2</code> (R2) is added to unnamed pool U2 (the last created
 * one). Step 7 will delete unnamed pool U2 (and resource R2), while step 8 will delete unnamed 
 * pool U1 (and resource R1).
 * <p> 
 * Once a persistent unnamed pool is created, a DELETE WIDGET-POOL statement will not be able to
 * delete it, if the persistent pool is on top of the stack.  For this reason, a 
 * CREATE WIDGET-POOL PERSISTENT statement will do nothing if the unnamed pool at the top of the
 * stack is persistent too. 
 * <p> 
 * When the current scope ends, all non-persistent unnamed pools will be deleted. But, if there
 * is an unnamed persistent pool on the top of the stack for scope S2, this one will be "leaked" 
 * to the previous scope, S1.  The reason for this is that if scope S1 has an active unnamed
 * non-persistent pool, then this pool must be deleted when S1 ends.  There are no restriction
 * for S1 to already have its own persistent unnamed pool. Thus, when S2 ends and leaks the top
 * persistent unnamed pool to S1, will result in S1 to have more than one persistent pool. But 
 * this will not alter how persistent pools behave for S1: non-persistent unnamed pools can still 
 * be created on top of the persistent unnamed pool, and unnamed pool deletion will stop when the
 * first persistent unnamed pool is back on the top of the stack.
 * <pre>
 *  1: CREATE WIDGET-POOL. // U1
 * 
 *  2: PROCEDURE proc0.
 *  3: CREATE WIDGET-POOL PERSISTENT. // U2
 *  4: // create resource R1
 *  5: DELETE WIDGET-POOL.
 *  6: END.
 * 
 *  7: RUN proc0.
 *  8: DELETE WIDGET-POOL.
 *  9: CREATE WIDGET-POOL. // U3
 * 10: // create resource R2 
 * 11: DELETE WIDGET-POOL.
 * 12: DELETE WIDGET-POOL.
 * 
 * 13: CREATE WIDGET-POOL. // U4
 * 14: // create resource R3
 * 
 * 15: CREATE WIDGET-POOL PERSISTENT. // U5
 * 16: // create resource R4
 * 17: DELETE WIDGET-POOL.
 * </pre>
 * <ul>
 * <li>step 1 creates an unnamed widget pool U1. The stack of unnamed pools for scope of the 
 *     external program (S1) contains only U1.</li>
 * <li>step 7 executes <code>proc0</code> which creates unnamed pool U2. On step 5, the active 
 *     pool contains resource R1, but will not be deleted. After <code>proc0</code> ends, pool U2
 *     is placed on top of the stack for scope S1, and the pool stack looks like [ U2 -&gt; U1 ].
 *     Resource R2 will not be deleted.</li>
 * <li>step 8 will try to delete the top of the stack, but U2 is persistent, so will be a no-op.
 *     </li>
 * <li>step 9 creats a new unnamed pool in scope S1, U3, and puts it on the top of the unnamed 
 *     pool stack for scope S1. The stack for S1 will look like [ U3 -&gt; U2 -&gt; U1 ]. </li>
 * <li>step 10 will create resource R2 and place it in unnamed pool U3 (as this is on top of the
 *     stack for scope S1).</li>
 * <li>step 11 will delete the unnamed pool on the top of the stack for scope S1, which is U3 and
 *     is non-persistent. This will delete resource R2 too.</li>
 * <li>step 12 will try to delete the unnamed pool on the top of the stack for scope S1, which 
 *     now is U2. But U2 is persistent, thus will not be deleted.</li>
 * <li>step 13 will create a new unnamed pool, U4, and place it on the top of stack for S1. The 
 *     stack of unnamed pools for S1 will look like: [ U4 -&gt; U2 -&gt; U1 ]</li>
 * <li>step 14 will create resource R3 and place it in pool U4 (top of stack for S1).</li>
 * <li>step 15 will create a new persistent unnamed pool, U5, for scope S1. The stack for S1 will
 *     look like: [ U5 -&gt; U4 -&gt; U2 -&gt; U1 ].</li>
 * <li>step 16 will create resource R4 and place it in pool U5 (top of stack for S1).</li>
 * <li>step 17 will try to delete the top unnamed pool for S1, U5, but this is persistent, so it
 *     will not be deleted. After this, pools U4, U2 and U1 remain hidden. When S1 ends, only the
 *     non-persistent unnamed pools (U1 and U4) will be deleted. Pools U2 and U5 will survive, and
 *     pool U5 will be placed on the top of the stack for the previous scope, U0.</li>
 * </ul>
 * <p>
 * This unnamed pool property which doesn't allow a persistent unnamed pool to be deleted will 
 * force it to exist for the duration of the user's session.
 * 
 * <h3>Named Pools</h3>
 * At a certain time, no more than one named pool may exist with a given name. When a scope ends,
 * all non-persistent named pools will be deleted. A persistent pool will survive until someone
 * calls a DELETE WIDGET-POOL statement for that persistent pool; this means a persistent pool
 * will survive even after the external procedure which created it is finished or gets deleted.
 * 
 * <h3>Resource management</h3>
 * When a resource (which can be pooled) is created, its IN WIDGET-POOL clause (if present) is
 * validated first. If a named pool is specified and it can't be found/isn't valid, then the 
 * resource will not be created.
 * <p>
 * Adding a resource to an unnamed pool will search for the first active unnamed pool, either in
 * current scope or any previous scope. Once a resource is added to a named or unnamed pool,
 * it will survive until that pool gets deleted (or the resource is deleted).
 * <p>
 * When an external procedure is ran persistent, all resources will survive until the procedure is
 * deleted; when the procedure is deleted, it will delete all the resources in the non-persistent
 * pools.
 */
public class WidgetPool
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(WidgetPool.class);
   
   /** A marker instance for an empty dequeu. */
   private static final Deque<Integer> EMPTY_DEQUE = new ArrayDeque<>();
   
   /** The widget pools are private per each context. */
   private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
   {
      protected WorkArea initialValue()
      {
         return new WorkArea();
      };
   };
   
   /**
    * Delete an unnamed pool.
    */
   public static void delete()
   {
      delete((character) null);
   }

   /**
    * Delete a pool with the given name.
    *
    * @param    name
    *           The name of the newly created pool. Specify <code>null</code> is the pool is 
    *           unnamed.
    */
   public static void delete(String name)
   {
      delete(name == null ? (character) null : new character(name));
   }

   /**
    * Delete a pool with the given name.
    *
    * @param    name
    *           The name of the newly created pool. Specify <code>null</code> is the pool is 
    *           unnamed.
    */
   public static void delete(character name)
   {
      if (name != null && name.isUnknown())
      {
         final String errMsg = "Unable to evaluate widget pool name";
         ErrorManager.recordOrThrowError(3177, errMsg, false);
         return;
      }
      
      WorkArea wa = locate();
      // ensure the current top-level block has widget pool support
      BlockDefinition block = wa.tm.nearestTopLevel();
      if (!block.widgetPool)
      {
         wa.scopeStart(block);
      }
      
      boolean unnamed = (name == null);
      String sname = (unnamed ? null : name.toStringMessage());
      WidgetPoolData data = findActivePool(wa, sname, true);
      
      if (data == null)
      {
         if (!unnamed)
         {
            final String msg = "Attempt to delete widget-pool %s, but it doesn't exist";
            ErrorManager.recordOrThrowError(4117, String.format(msg, sname), false);
         }
         
         return;
      }

      deletePool(wa, data, false);
   }

   /**
    * Create an unnamed pool.
    */
   public static void create()
   {
      create((character) null);
   }

   /**
    * Create an unnamed pool.
    * 
    * @param    persistent
    *           <code>true</code> if the pool is persistent.
    */
   public static void create(boolean persistent)
   {
      create((character) null, persistent);
   }

   /**
    * Create a pool with the given name.
    *
    * @param    name
    *           The name of the newly created pool. Specify <code>null</code> is the pool is 
    *           unnamed.
    */
   public static void create(character name)
   {
      create(name, false);
   }

   /**
    * Create a pool with the given name.
    *
    * @param    name
    *           The name of the newly created pool. Specify <code>null</code> is the pool is 
    *           unnamed.
    */
   public static void create(String name)
   {
      create(name == null ? (character) null : new character(name), false);
   }

   /**
    * Create a pool with the given name and the given "persistent" option.
    *
    * @param    name
    *           The name of the newly created pool. Specify <code>null</code> is the pool is 
    *           unnamed.
    * @param    persistent
    *           <code>true</code> if the pool is persistent.
    */
   public static void create(String name, boolean persistent)
   {
      create(name == null ? (character) null : new character(name), persistent);
   }

   /**
    * Create a pool with the given name and the given "persistent" option.
    *
    * @param    name
    *           The name of the newly created pool. Specify <code>null</code> is  the pool is 
    *           unnamed.
    * @param    persistent
    *           <code>true</code> if the pool is persistent.
    */
   public static void create(character name, boolean persistent)
   {
      if (name != null)
      {
         if (name.isUnknown())
         {
            final String errMsg = "Unable to evaluate widget pool name";
            ErrorManager.recordOrThrowError(3177, errMsg, false);
            return;
         }
         else if (name.toStringMessage().length() == 0)
         {
            final String errMsg = "Attempt to create a widget-pool with a zero-length name";
            ErrorManager.recordOrThrowError(4115, errMsg, false);
            return;
         }
      }

      boolean unnamed = (name == null);
      String sname = (unnamed ? null : name.toStringMessage());
      WorkArea wa = locate();
      
      // ensure the current top-level block has widget pool support
      BlockDefinition block = wa.tm.nearestTopLevel();
      if (!block.widgetPool)
      {
         wa.scopeStart(block);
      }
      
      WidgetPoolData data = findActivePool(wa, sname, true);
      
      if (data != null && !unnamed)
      {
         // error is shown only in case there is already a named pool
         final String msg = "Attempt to create widget-pool %s, but it already exists";
         ErrorManager.recordOrThrowError(4116, String.format(msg, sname), false);

         return;
      }

      // any existing unnamed pools will be hidden in this scope.
      data = newPool(wa, sname, persistent);
   }

   /**
    * Adds the specified resource to the pool. Although this is exposed for the public, do not 
    * used outside of P2J code. Is made public because access to it is needed from any P2J 
    * package.
    * 
    * @param    resource
    *           The resource to be pooled.
    * @param    name
    *           The pool name, set to <code>null</code> if the unnamed pool is to be used.
    *           
    * @throws   IllegalStateException
    *           The exception is throw if the call of this method targets a named pool (the 
    *           <code>name</code> parameter is specified) and either the name is unknown or a pool
    *           with the specified name does not exist.
    *           <p>
    *           If the exception is thrown, there must be an error in the code which creates the
    *           resource. That code must validate the pool name (by calling 
    *           {@link WidgetPool#validWidgetPool}) before any other resource-related validation
    *           and before the resource is created.
    */
   public static void addResource(WrappedResource resource, character name)
   {
      addResource(null, resource, name);
   }
   
   /**
    * Adds the specified resource to the pool. Although this is exposed for the public, do not 
    * used outside of P2J code. Is made public because access to it is needed from any P2J 
    * package.
    * 
    * @param    wa
    *           The {@link WorkArea} instance.
    * @param    resource
    *           The resource to be pooled.
    * @param    name
    *           The pool name, set to <code>null</code> if the unnamed pool is to be used.
    *           
    * @throws   IllegalStateException
    *           The exception is throw if the call of this method targets a named pool (the 
    *           <code>name</code> parameter is specified) and either the name is unknown or a pool
    *           with the specified name does not exist.
    *           <p>
    *           If the exception is thrown, there must be an error in the code which creates the
    *           resource. That code must validate the pool name (by calling 
    *           {@link WidgetPool#validWidgetPool}) before any other resource-related validation
    *           and before the resource is created.
    */
   public static void addResource(WorkArea wa, WrappedResource resource, character name)
   {
      if (wa == null)
      {
         wa = locate();
      }
      
      BlockDefinition blk = wa.tm.nearestTopLevel();
      if (!blk.widgetPool)
      {
         wa.scopeStart(blk);
      }
      
      boolean unnamed = (name == null);
      String sname = (unnamed ? null : name.toStringMessage());
      
      WidgetPoolData data = (name != null && name.isUnknown() ? null 
                                                              : findActivePool(wa, sname, false));
      if (data == null)
      {
         if (!unnamed)
         {
            final String msg = 
               "Could not determine named pool '%s'. You need to validate before adding the " +
               "resource!";
            throw new IllegalStateException(String.format(msg, sname));
         }

         // no pool is active, return
         return;
      }

      data.addResource(resource);
      
      // save the resource, to be able to determine its pool later
      wa.poolForResource.put(resource, data.id);
   }
   
   /**
    * Determine if the specified <code>name</code> targets a known widget pool.
    * <p>
    * A missing unnamed pool will be an error.
    * 
    * @param    name
    *           The pool name, set to <code>null</code> if the unnamed pool is to be used.
    *           
    * @return   <code>true</code> if a pool was found with the specified name.
    */
   public static boolean validWidgetPool(character name)
   {
      // an unnamed widget-pool name is always valid.
      if (name == null)
      {
         return true;
      }
      
      if (name.isUnknown())
      {
         final String errMsg = "Unable to evaluate widget pool name";
         ErrorManager.recordOrThrowError(3177, errMsg, false);
         return false;
      }
      
      String sname = name.toStringMessage();
      
      WidgetPoolData data = findActivePool(locate(), sname, false);
      if (data == null)
      {
         // error is shown only if named pool is not found
         final String msg = 
            "Trying to create a widget in widget-pool '%s', but pool does not exist";
         ErrorManager.recordOrThrowError(3178, String.format(msg, sname), false);

         return false;
      }
      
      return true;
   }
   
   /**
    * Get an instance of {@link WidgetPoolHelper}, which has the context-local instance saved,
    * so API access can be done without a context-local query.
    * 
    * @return   See above.
    */
   static WidgetPoolHelper getHelper()
   {
      return new WidgetPoolHelper(locate());
   }
   
   /**
    * Locate the context's {@link WorkArea} instance.
    * 
    * @return   See above.
    */
   private static WorkArea locate()
   {
      WorkArea wa = local.get();
      if (wa.tm == null)
      {
         wa.tm = TransactionManager.getTransactionHelper();
         wa.pm = ProcedureManager.getProcedureHelper();
         wa.oh = ObjectOps.getObjectHelper();
      }
      
      return wa;
   }
   
   /***
    * Create a new pool and register it in the appropriate maps/sets.
    * 
    * @param    wa
    *           The context-local state in {@link WorkArea}.
    * @param    name
    *           The name of this pool. Is <code>null</code> if the pool is unnamed.
    * @param    persistent
    *           Flag identifying if this pool is persistent.
    *           
    * @return   The created {@link WidgetPoolData definition} for the new pool.
    */
   private static WidgetPoolData newPool(WorkArea wa, String name, boolean persistent)
   {
      boolean unnamed = (name == null);
      
      if (unnamed && persistent)
      {
         Integer top = wa.unnamedPoolScopes.peek().peek();
         if (top != null && wa.persistentPools.contains(top))
         {
            // if the top pool is persistent, then do not create another one. even if 4GL might
            // create a different pool, there is no reason to do so, because we can't pop from
            // the stack a persistent unnamed pool.
            return wa.pools.get(top);
         }
      }
      
      int id = Utils.uniqueId();
      WidgetPoolData data = new WidgetPoolData(id, name);
      wa.pools.put(id, data);

      if (unnamed)
      {
         wa.unnamedPools.add(id);
      }
      else
      {
         wa.namedPools.put(data.key, id);
      }
      
      if (persistent)
      {
         wa.persistentPools.add(id);
      }
      
      if (unnamed)
      {
         Deque<Integer> ups = wa.unnamedPoolScopes.peek();
         if (ups == EMPTY_DEQUE)
         {
            ups = new ArrayDeque<>();
            wa.unnamedPoolScopes.pop();
            wa.unnamedPoolScopes.push(ups);
         }
         ups.push(id);
      }
      else
      {
         Set<Integer> nps = wa.namedPoolScopes.peek();
         if (nps == Collections.EMPTY_SET)
         {
            nps = new HashSet<>();
            wa.namedPoolScopes.pop();
            wa.namedPoolScopes.push(nps);
         }
         nps.add(id);
      }
      
      // collect the pools for each external program
      Object oThis = wa.pm._thisProcedure();
      
      Set<Integer> thisIds = wa.poolsPerExtProg.get(oThis);
      if (thisIds == null)
      {
         thisIds = new HashSet<Integer>();
         wa.poolsPerExtProg.put(oThis, thisIds);
      }
      thisIds.add(id);
      
      // save the procedure which created this pool
      wa.procForPool.put(id, oThis);
      
      return data;
   }

   /**
    * Filter the set of pool IDs and from it, delete only the non-persistent widget-pools.
    * 
    * @param    wa
    *           The context-local state in {@link WorkArea}.
    * @param    ids
    *           A set of pool IDs.
    * @param    fromScopeFinished
    *           Flag indicating that this is called from {@link WorkArea#scopeFinished}.
    */
   private static void deleteNonPersistentPools(WorkArea wa, Set<Integer> ids, boolean fromScopeFinished)
   {
      if (ids.isEmpty())
      {
         return;
      }
      
      // determine the non-persistent pools
      Set<Integer> nonPersistent = new HashSet<>(ids);
      nonPersistent.removeAll(wa.persistentPools);
      
      // delete the non-persistent pools; persistent pools will survive, even after a procedure 
      // ran non-persistent is finished (or deleted).
      for (int id : nonPersistent)
      {
         WidgetPoolData data = getPool(wa, id);
         deletePool(wa, data, fromScopeFinished);
      }
   }
   
   /**
    * Delete the specified pool. Note that an unnamed persistent pool will not be deleted.
    * <p>
    * When a pool is deleted, all its contained resources will be deleted too.
    * 
    * @param    wa
    *           The context-local state in {@link WorkArea}.
    * @param    data
    *           The definition of the pool to be deleted.
    * @param    fromScopeFinished
    *           Flag indicating that this is called from {@link WorkArea#scopeFinished}.
    */
   private static void deletePool(WorkArea wa, WidgetPoolData data, boolean fromScopeFinished)
   {
      int id = data.id;

      boolean unnamed    = wa.unnamedPools.contains(id);
      boolean named      = wa.namedPools.containsKey(data.key);
      boolean persistent = wa.persistentPools.contains(id);
      
      if (unnamed && persistent)
      {
         // the unnamed persistent pool can't be deleted
         return;
      }
      
      // ensure the current top-level block has widget pool support
      boolean usePrev = wa.pm.inNotifications();
      fromScopeFinished = fromScopeFinished || (wa.oh.inScopeFinished() && wa.tm.nearestTopLevel().widgetPool);
      BlockDefinition block = usePrev ? wa.tm.prevTopLevel() : wa.tm.nearestTopLevel();
      if (!block.widgetPool)
      {
         Set<Integer> namedScope = null;
         Deque<Integer> unnamedScope = null;
         // if we are executing current block's WidgetPool.scopeFinished, we need to pop/push the block's scope
         // otherwise, even if we are in notifications, the current block's scope was either non-existent or
         // already popped
         if (usePrev && fromScopeFinished)
         {
            namedScope = wa.namedPoolScopes.pop();
            unnamedScope = wa.unnamedPoolScopes.pop();
         }
         wa.scopeStart(block);
         if (usePrev && fromScopeFinished)
         {
            wa.namedPoolScopes.push(namedScope);
            wa.unnamedPoolScopes.push(unnamedScope);
         }
      }

      // delete the resources; this will clean the resource-related maps
      data.deleteResources();

      // clean up the registry
      wa.pools.remove(id);
      if (unnamed)
      {
         wa.unnamedPools.remove(id);
      }
      else
      {
         wa.namedPools.remove(data.key);
      }
      wa.persistentPools.remove(id);
      // remove from the external program's set too.
      Object oThis = wa.procForPool.remove(id);
      Set<Integer> ids = wa.poolsPerExtProg.get(oThis);
      if (ids != null)
      {
         // ids may be null if the program which created this pool has been deleted.
         ids.remove(id);
      }

      // the other maps/sets
      if (unnamed)
      {
         Deque<Integer> ups = wa.unnamedPoolScopes.peek();
         if (ups != null && !ups.isEmpty())
         {
            ups.remove(id);
         }
      }
      else if (!wa.namedPoolScopes.isEmpty())
      {
         // this might be from a previous scope, so need to walk the stack
         for (Set<Integer> namedPools : wa.namedPoolScopes)
         {
            if (namedPools.remove(id))
            {
               // terminate on first found
               break;
            }
         }
      }
   }
   
   /**
    * Determine the active pool. If <code>name</code> is not <code>null</code>, than the active
    * named pool will be used.
    * 
    * @param    wa
    *           The context-local state in {@link WorkArea}.
    * @param    name
    *           The pool name. May be <code>null</code> for an unnamed pool.
    * @param    currentScope
    *           <code>true</code> when search must be done only in current scope.
    * @return   <code>null</code> if no pool is found, else its 
    *           {@link WidgetPoolData definition} is returned.
    */
   private static WidgetPoolData findActivePool(WorkArea wa, String name, boolean currentScope)
   {
      Integer id = null;
      boolean unnamed = (name == null);
      
      if (!unnamed)
      {
         // get the named pool
         String key = name.toLowerCase();
         id = wa.namedPools.get(key);
      }
      else if (wa.unnamedPools.size() > 0)
      {
         // if no registered unnamed pool, then no reason to search down the stack

         // get the unnamed pool for this scope; will be null if no unnamed in stack. always
         // check down the stack(s) until one is found
         for (Deque<Integer> stack : wa.unnamedPoolScopes)
         {
            if (!stack.isEmpty())
            {
               id = stack.peek();
               break;
            }
            
            if (currentScope)
            {
               // exit on first scope, if needed
               break;
            }
         }
      }

      return (id == null ? null : getPool(wa, id));
   }
   
   /**
    * Get the {@link WidgetPoolData pool} with the given ID.
    * 
    * @param    wa
    *           Context-local work area.
    * @param    id
    *           The pool's ID.
    *           
    * @return   See above.
    */
   private static WidgetPoolData getPool(WorkArea wa, int id)
   {
      return wa.pools.get(id);
   }

   /**
    * Helper to expose APIs which have direct access to the context-local state.
    */
   static class WidgetPoolHelper
   {
      /** The {@link WorkArea} instance. */
      private final WorkArea wa;
      
      /**
       * Create a new instance and associate the given WorkArea instance.
       * 
       * @param    wa
       *           The {@link WorkArea} instance.
       */
      public WidgetPoolHelper(WorkArea wa)
      {
         this.wa = wa;
      }
      

      /**
       * Adds the specified resource to the pool. Although this is exposed for the public, do not 
       * used outside of P2J code. Is made public because access to it is needed from any P2J 
       * package.
       * 
       * @param    resource
       *           The resource to be pooled.
       * @param    name
       *           The pool name, set to <code>null</code> if the unnamed pool is to be used.
       *           
       * @throws   IllegalStateException
       *           The exception is throw if the call of this method targets a named pool (the 
       *           <code>name</code> parameter is specified) and either the name is unknown or a pool
       *           with the specified name does not exist.
       *           <p>
       *           If the exception is thrown, there must be an error in the code which creates the
       *           resource. That code must validate the pool name (by calling 
       *           {@link WidgetPool#validWidgetPool}) before any other resource-related validation
       *           and before the resource is created.
       */
      public void addResource(WrappedResource resource, character name)
      {
         WidgetPool.addResource(wa, resource, name);
      }
      
      /**
       * Checks if the given referent has an unnamed pool registered.
       * 
       * @param    referent
       *           The referent to check.
       *           
       * @return   <code>true</code> if there is at least one unnamed pool for this referent.
       */
      public boolean hasUnnamedPool(Object referent)
      {
         Deque<Integer> up = wa.unnamedProcPools.get(referent);
         
         return up != null && !up.isEmpty();
      }
      
      /**
       * Delegates the call to {@link WorkArea#scopeStart}
       * 
       * @param    block
       *           The explicit block definition which required this notification.
       */
      public void scopeStart(BlockDefinition block)
      {
         wa.scopeStart(block);
      }
      
      /**
       * Delegates the call to {@link WorkArea#scopeFinished(Object, boolean, boolean)}
       */
      public void scopeFinished(Object oThis, boolean external, boolean persistent)
      {
         wa.scopeFinished(oThis, external, persistent);
      }
      
      /**
       * Remove the given resource from its pool, if it was pooled.
       * 
       * @param    resource
       *           The resource to be removed.
       */
      public void removeResource(WrappedResource resource)
      {
         Integer poolId = wa.poolForResource.remove(resource);
         
         // nothing more to do if the resource wasn't pooled
         if (poolId == null)
         {
            return;
         }
         
         WidgetPoolData data = getPool(wa, poolId);
         if (data == null)
         {
            // this resource wasn't pooled, so nothing to do
            return;
         }
         
         data.removeResource(resource);
      }
      
      /**
       * Delete all the non-persistent widget-pools associated with the specified procedure.
       * 
       * @param    proc
       *           The external procedure instance.
       */
      public void deleteWidgetPools(Object proc)
      {
         // an external procedure was deleted, this acts like it's scope has finished.
         Set<Integer> ids = wa.poolsPerExtProg.get(proc);
         if (ids != null)
         {
            deleteNonPersistentPools(wa, ids, false);
            
            // remove this from the map, is no longer needed
            wa.poolsPerExtProg.remove(proc);
         }
         wa.unnamedProcPools.remove(proc);
      }
   }
   
   /**
    * Container for the widget-pool data.
    */
   private static class WorkArea
   {
      /** Helper to use the TM without any context local lookups. */
      private TransactionManager.TransactionHelper tm = null;

      /** Helper to use the ProcedureManager without any context local lookups. */
      private ProcedureManager.ProcedureHelper pm = null;

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

      /** A registry of pools, per its ID. */
      private final Map<Integer, WidgetPoolData> pools = new HashMap<>();
      
      /** Pool names are global, so keep a map of them (key is always lowercased). */
      private final Map<String, Integer> namedPools = new HashMap<>();
      
      /** A set containing the ID of all unnamed pools. */
      private final Set<Integer> unnamedPools = new HashSet<>();
      
      /** A set containing the ID of all persistent pools. */
      private final Set<Integer> persistentPools = new HashSet<>();
      
      /** Identify the pool to which a resource was added (if was pooled). */
      private final Map<WrappedResource, Integer> poolForResource = new HashMap<>();
      
      /** Identify the external procedure used to create the pool. */
      private final Map<Integer, Object> procForPool = new HashMap<>();
      
      /** Collect all the created pools for an external program. */
      private final Map<Object, Set<Integer>> poolsPerExtProg = new IdentityHashMap<>();
      
      /** Keep the scopes of named pools. */
      private final Deque<Set<Integer>> namedPoolScopes = new ArrayDeque<>();
      
      /** Keep the scopes of unnamed pools (only one can be active at a certain time). */
      private final Deque<Deque<Integer>> unnamedPoolScopes = new ArrayDeque<>();
      
      /** A map of the unnamed pools active at the time the persistent pool has executed. */
      private final Map<Object, Deque<Integer>> unnamedProcPools = new IdentityHashMap<>();
      
      /** A cached set instance used by {@link #scopeFinished}. */
      private final Set<Integer> ids = new HashSet<>();
      
      /**
       * Notify a new scope has started; invoked by {@link ProcedureManager}'s scoping support when
       * a new scope is started.
       * <p>
       * Notification will be performed only on the start of a top-level block (external program,
       * procedure, function or trigger).
       * <p>
       * No default unnamed pool will be created. An unnamed pool must be explicitly created to be 
       * used.
       * 
       * @param    block
       *           The explicit block definition which required this notification.
       */
      public void scopeStart(BlockDefinition block)
      {
         if (block.type == BlockType.UNKNOWN)
         {
            // do nothing for the global block (like for appservers, where arguments are processed)
            return;
         }
         
         block.widgetPool = true;
         
         Deque<Integer> ups = EMPTY_DEQUE;
         
         if (block.type != BlockType.EXTERNAL_PROC)
         {
            // case 1: even if called from 'scopeFinished', still uses 'this-procedure', as when this is 
            // called, ProcedureManager.scopeFinished already popped the currently processed 'this-procedure'.
            // so we target the previous scope 'naturally'
            
            // case 2: when implicit object deletion happens, this will be performed from within 'popScope',
            // but before ProcedureManager.scopeFinished happens.  So THIS-PROCEDURE is still on stack - in 
            // such cases, we need to use 'prev-this-procedure'.
            
            ProcedureData pd = oh.inScopeFinished() ? pm.prevThisProcedureData() : pm.thisProcedureData();
            if (pd.isPersistent())
            {
               Object referent = pd.getReferent();
               if (unnamedProcPools.containsKey(referent))
               {
                  ups = new ArrayDeque<>();
                  ups.addAll(unnamedProcPools.get(referent));
               }
            }
         }

         namedPoolScopes.push(Collections.emptySet());
         unnamedPoolScopes.push(ups);
      }
      
      /**
       * Notify a scope has finished; invoked by {@link ProcedureManager}'s scoping support when a
       * scope is finished.
       * <p>
       * This will delete the created non-persistent pools, but only if this is not a notification 
       * of an external procedure which was ran persistent.
       * <p>
       * Notification will be performed only on the end of a top-level block (external program,
       * procedure, function or trigger).
       * 
       * @param    oThis
       *           Current THIS-PROCEDURE handle.
       * @param    external
       *           Flag indicating that this is an external procedure.
       * @param    persistent
       *           Flag indicating that this external procedure is ran persistent.
       */
      public void scopeFinished(Object oThis, boolean external, boolean persistent)
      {
         ids.clear();
         
         // compute the set of active pools
         Set<Integer> topNamed = namedPoolScopes.peek();
         if (!topNamed.isEmpty())
         {
            ids.addAll(topNamed);
         }
         Deque<Integer> topUnnamed = unnamedPoolScopes.peek();
         if (!topUnnamed.isEmpty())
         {
            ids.addAll(topUnnamed);
         }
         
         if (!external && unnamedProcPools.containsKey(oThis))
         {
            // the external program's pool get deleted when the extprog gets deleted
            Deque<Integer> oThisPools = unnamedProcPools.get(oThis);
            if (!oThisPools.isEmpty())
            {
               ids.removeAll(oThisPools);
               topUnnamed.removeAll(oThisPools);
            }
         }

         if (!(external && persistent))
         {
            deleteNonPersistentPools(this, ids, true);
            
            if (external)
            {
               poolsPerExtProg.remove(oThis);
               unnamedProcPools.remove(oThis);
            }
         }
         else
         {
            // if finishing an external program ran in persistent mode, let the pools survive; 
            // they will be deleted when the external program gets deleted
         }
         
         // this must be done at this step - deleteNonPersistentPools is dependent on it
         namedPoolScopes.pop();
         Deque<Integer> unnamedIds = unnamedPoolScopes.pop();
         
         if (!persistent && !unnamedIds.isEmpty())
         {
            // we need to ensure the previous top-level block has widgetPool support
            // this needs to be done after we remove the current widget pool state for the current block
            BlockDefinition block = tm.prevTopLevel();
            if (block != null && !block.widgetPool)
            {
               block.widgetPool = true;
               
               Deque<Integer> ups = EMPTY_DEQUE;
               ProcedureData prev = pm.prevThisProcedureData();
               if (block.type != BlockType.EXTERNAL_PROC && prev.isPersistent())
               {
                  Object referent = prev.getReferent();
                  if (unnamedProcPools.containsKey(referent))
                  {
                     ups = new ArrayDeque<>();
                     ups.addAll(unnamedProcPools.get(referent));
                  }
               }

               namedPoolScopes.push(Collections.emptySet());
               unnamedPoolScopes.push(ups);
            }
         }
         
         // at the time of the pop, all the non-persistent pools have been removed. now, check if
         // a persistent unnamed pool is in effect. if so, "leak" it to the previous scope.
         // any pool created in a persistent procedure will not be leaked to a previous scope. The
         // persistent procedure's pools (named or unnamed, persistent or not) will be deleted when
         // the persistent procedure's handle gets deleted.
         if (!persistent && !unnamedPoolScopes.isEmpty() && !unnamedIds.isEmpty())
         {
            // what is left must be a persistent unnamed pool.
            int id = unnamedIds.pop();
            if (!unnamedIds.isEmpty())
            {
               if (LOG.isLoggable(Level.SEVERE))
               {
                  final String msg = "Scope [%d] should have no unnamed pools left!";
                  LOG.log(Level.SEVERE, String.format(msg, unnamedPoolScopes.size() + 1));
               }
            }
            
            Deque<Integer> prevScope = unnamedPoolScopes.peek();
            int prev = prevScope.isEmpty() ? -1 : prevScope.peek();
            if (persistentPools.contains(prev))
            {
               // merge all from this persistent pool to the prev pool
               WidgetPoolData prevData = pools.get(prev);
               WidgetPoolData currentData = pools.get(id);
               
               for (WrappedResource res : currentData.resources)
               {
                  prevData.addResource(res);
                  poolForResource.put(res, prev);
               }
               
               // remove this pool, as all its resources were leaked to the previous scope
               persistentPools.remove(id);
               pools.remove(id);
               unnamedPools.remove(id);
               Set<Integer> pools = poolsPerExtProg.get(oThis);
               if (pools != null)
               {
                  poolsPerExtProg.get(oThis).remove(id);
               }
               procForPool.remove(id);
            }
            else
            {
               if (!pools.containsKey(id))
               {
                  throw new RuntimeException("The WidgetPool " + id + " has already been deleted!");
               }
               // "leak" it to the prev scope
               Deque<Integer> ups = unnamedPoolScopes.peek();
               if (ups == EMPTY_DEQUE)
               {
                  ups = new ArrayDeque<>();
                  unnamedPoolScopes.pop();
                  unnamedPoolScopes.push(ups);
               }
               ups.push(id);
               
               // re-adjust the pool's creator as the one from the prev scope
               Object referent = pm._thisProcedure();
               Set<Integer> pools = poolsPerExtProg.get(referent);
               if (pools == null)
               {
                  pools = new HashSet<>();
                  poolsPerExtProg.put(referent, pools);
               }
               pools.add(id);
               
               procForPool.put(id, referent);
            }
         }
         else if (persistent && external && !unnamedIds.isEmpty())
         {
            unnamedProcPools.put(oThis, unnamedIds);
         }
      }
   }
   
   /**
    * The data needed for a widget pool, named or unnamed.
    */
   private static class WidgetPoolData
   {
      /** Set of resources created in this widget pool. */
      private final Set<WrappedResource> resources = new LinkedHashSet<>();
      
      /** The ID of this widget pool. */
      private final int id;
      
      /** This pool's key (to identify it by name, if this is a named pool). */
      private final String key;
      
      /**
       * Create a new widget pool.
       * 
       * @param    id
       *           The ID of this widget pool.
       * @param    name
       *           The name of this widget pool. <code>null</code> if unnamed. 
       */
      public WidgetPoolData(int id, String name)
      {
         this.id = id;
         this.key = (name == null ? null : name.toLowerCase());
      }
      
      /**
       * Add a new resource to this widget pool.
       * 
       * @param    resource
       *           The resource to be added.
       */
      public void addResource(WrappedResource resource)
      {
         resources.add(resource);
      }
      
      /**
       * Remove the resource from this widget pool.
       * 
       * @param    resource
       *           The resource to be removed.
       */
      public void removeResource(WrappedResource resource)
      {
         resources.remove(resource);
      }
      
      /**
       * Delete all the resources from this pool.
       */
      public void deleteResources()
      {
         // use a copy, as the resource-related maps/sets get cleaned when the resource is deleted
         Set<WrappedResource> copy = new LinkedHashSet<WrappedResource>(resources);

         for (WrappedResource res : copy)
         {
            if (res.valid())
            {
               HandleOps.delete(new handle(res), false);
            }
         }
      }
   }
}