# AppServerManager.java

    /*
    ** Module   : AppServerManager.java
    ** Abstract : Defines APIs to manage the appservers defined by this P2J server and any incoming
    **            remote API calls for these appservers.
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20130529 Created initial version.
    ** 002 CA  20130628 Context-local data for this class must not be reset by the agent.  Added a
    **                  listener to automatically disconnect if the remote side has closed its session
    **                  while the connection is still active.
    ** 003 CA  20130705 Reworked the appserver launching.
    ** 004 CA  20130822 Fixed server disconnection when the P2J client ends abruptly, while still
    **                  connected to a local appserver.
    ** 005 OM  20131018 Updated to conform the new SessionListener API.
    ** 006 CA  20140326 The remote persistent procedures need to be deleted by the Agent which created
    **                  them.
    ** 007 CA  20140826 Fixed agent context cleanup if the P2J session is terminated before the
    **                  appserver connection is establised.
    ** 008 CA  20141124 Fixed uniqueId() - the nextId field was not being incremented properly, making
    **                  execution of async requests impossible.
    ** 009 IAS 20160331 Fixed appServers.remove
    ** 010 CA  20160404 Enhanced the appserver invocations, to allow implicit or explicit termination
    **                  of requests being executing on a certain connection.
    ** 011 CA  20160405 Fixed H010 - during CONNECT procedure call, activeAgents can not be tracked.
    ** 012 CA  20180503 Added SESSION:SERVER-CONNECTION-ID support.
    ** 013 CA  20190612 Added APIs to create remote legacy objects and invoke methods on them.
    ** 014 CA  20190628 Fixed Agent disconnect in State-Free mode.  Misc improvements.
    **     CA  20190703 Added WebHandler service support.
    ** 015 CA  20190811 Fixed initRemoteAppServerswhen there are no app_services nodes.
    ** 016 CA  20200427 Added support for SERVER-CONNECTION-BOUND, SERVER-CONNECTION-CONTEXT,
    **                  SERVER-CONNECTION-BOUND-REQUEST and OERequestInfo.
    **     CA  20200514 Fixed error handling for handleWebService.
    **                  Fixed error handling when no agent is available.
    ** 029 IAS 20200722 Improved logging.
    ** 030 CA  20201027 The remote side can delete the IN HANDLE used by the requester at the RUN statement.
    ** 031 CA  20211112 Fixed getServerOperatingMode() for multi-session agent appservers.
    **     CA  20211214 For State-free appservers, the agents can be bound to remote persistent procedures; this
    **                  binding must be made using the agent's ID, as a client can invoke multiple remote
    **                  persistent procedures, and using the procedure's ID for this binding can lead to
    **                  collisions, as the pair (connection ID, procedure ID) is not guaranteed to be unique for
    **                  a connection.
    **     CA  20211216 Fixed a bug in previous changes, for SINGLE-RUN/SINGLETON invocations - in this case, the
    **                  agent must be resolved, as there is no bound agent for the call.
    **     CA  20220104 Fixed Stateless appserver mode, which got broken in CA/20211214 - now the agent tracks any
    **                  persistent procedure ran over it.
    **     CA  20220111 Fixed SESSION:SERVER-CONNECTION-CONTEXT - this value must be shared to any agent running
    **                  requests for a connection.
    **     CA  20220208 For calls originating from outside the FWD server, do not serialize the legacy OO errors.
    **     CA  20220329 Added support for REST services written directly in Java.
    **     CA  20220405 Added authentication and authorization for web requests.  When this is enabled, the target
    **                  API call will be executed under the authenticated FWD context, and not the agent's context.
    **     CA  20220428 Logging improvements.
    **     SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
    ** 030 GBB 20230403 Add LegacyLogManagerConfigs param to startAppServer to be able to store and restore
    **                  LOG-MANAGER configs in State-reset mode.
    ** 031 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 032 CA  20230620 Initialize the app_services configuration for remote appservers on server startup, which
    **                  will prevent the FWD server to start if there are configuration errors.
    ** 033 HC  20230707 Fixed an NPE when sessionFree app server directory value not defined.
    ** 034 CA  20230712 Added 'invokeWithArgs', an API which can send any number of arguments to a remote program
    **                  acting as a controller, which can prepare and execute the real target.
    ** 035 GBB 20230825 SecurityManager csession methods calls updated.
    ** 036 CA  20240225 Fixed comments in startAppServer to better specify that this is used only on server-side.
    ** 037 GBB 20240318 Redundant methods lookupP2JServer, lookupP2JAccount, lookupTargetAppserver deleted.
    **                  Class RemoteAppServer made public.
    **                  Adding error handling for all cases in lookupRemoteAppServer.
    **                  Handling responses from appserver remote proc invocation (return-value, error and stop).
    ** 038 CA  20240620 Backed out part of H037 - the client needs to handle the response's return-value, error
    **                  condition and stop; AppServerManager is executed on the remote side, not on the client-side.
    ** 039 GBB 20240621 Connection error messages fixed.
    **                  Fix for connect without session model for session-managed appserver.
    ** 040 CA  20240729 Fixed deactivate on procedure delete: the deactivate procedure must be invoked before the
    **                  agent gets released.
    ** 041 DDF 20240923 Filter found servers based on the service name provided.
    ** 042 CA  20241113 Allow posting a task on the agent running in the current context - this can be used to
    **                  post tasks from different threads sharing the agent's context.
    ** 043 GBB 20250403 Support multi-session appservers in applicable methods.
    ** 044 CA  20250324 Improvements for remote FWD OpenClient connections: ensure that the remote side uses a
    **                  single connection to the FWD server, and also use a client-side pool of worker threads to
    **                  send the requests (the pool size is in sync with the maximum agents available to process
    **                  requests).
    */
    /*
    ** 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.concurrent.*;
    import java.util.concurrent.atomic.*;
    import java.util.logging.*;
    import java.util.stream.*;

    import javax.servlet.http.*;

    import com.goldencode.p2j.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.net.Session;
    import com.goldencode.p2j.oo.web.WebHandler;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.appserver.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * Manage the appservers defined by this P2J server by:
     * <ul>
     * <li>starting the agents when the appserver is started</li>
     * <li>listening and handling all incoming requests for the managed appservers</li>
     * </ul>
     * Also, it manages the appservers known to this P2J server. These known appservers are
     * disambiguated using the data configured in directory,  under the "server/default/app_services"
     * node.
     */
    public class AppServerManager
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(AppServerManager.class);

       /** Provides a map of default appService names, for each name server. */
       private static Map<String, String> nameServerDefaults = null;

       /** The list of the known, remote, appservers. */
       private static ArrayList<RemoteAppServer> remoteAppServers = null;

       /** Map of subject IDs to appserver names. */
       private static final Map<String, String> subjectToAppserver = new HashMap<>();

       /** Map of appservers to their associated pool, managed by this P2J server. */
       private static final Map<String, AgentPool> appServers = new HashMap<>();

       /** A map of authorized sessions which can access the given connection ID. */
       private static final Map<String, Set<Integer>> authorizedSessions = new HashMap<>();

       /** The session listeners registered for each connection. */
       private static final Map<String, SessionListener> sessionListeners = new ConcurrentHashMap<>();

       /** Map of executing requests to their associated Agents. */
       private static final Map<Integer, Agent> requests = new HashMap<>();

       /** Set of stopped requests. */
       private static final Set<Integer> stoppedRequests = new HashSet<>();

       /**
        * For each connection ID, this map includes the Agents which are currently executing requests.
        */
       private static final Map<String, Set<Agent>> activeAgents = new HashMap<>();

       /** Lock used to sync access to {@link #requests} and {@link #stoppedRequests}. */
       private static final Object lock = new Object();

       /** The next request ID. */
       private static int nextId = 0;

       /**
        * This context-local data is used only to determine which appserver is authenticated and its
        * mode (manager or agent).
        */
       private static ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
       {
          protected WorkArea initialValue()
          {
             return new WorkArea();
          };

          protected boolean isResetAllowed()
          {
             return false;
          };
       };

       /**
        * Init the data related to remote appservers, from the directory. It reads all the appservers
        * configured under the "server/default/app_services" node.
        */
       public static synchronized void initRemoteAppServers()
       {
          // do this only once
          if (nameServerDefaults != null)
          {
             return;
          }

          nameServerDefaults = new HashMap<String, String>();
          remoteAppServers = new ArrayList<RemoteAppServer>();
          // access the directory service
          DirectoryService ds = DirectoryService.getInstance();

          if (!ds.bind())
             throw new RuntimeException("directory bind failed");

          String appSrvPath = Utils.findDirectoryNodePath(ds, "app_services", "container", false);
          String[] serviceNodes = appSrvPath == null ? null : ds.enumerateNodes(appSrvPath);
          if (serviceNodes != null)
          {
             for (int i = 0; i < serviceNodes.length; i++)
             {
                String name = serviceNodes[i];
                String path = appSrvPath + "/" + name;

                RemoteAppServer data = new RemoteAppServer(ds, appSrvPath, name);

                String[] defaults = ds.getNodeStrings(path + "/defaults", "values");
                if (defaults != null)
                {
                   for (int j = 0; j < defaults.length; j++)
                   {
                      String ns = defaults[j];

                      if (nameServerDefaults.containsKey(ns))
                      {
                         throw new RuntimeException("More than one default service specified for nameserver " +
                                                    ns + "!  A namespace must appear as default for a single " +
                                                    "'app_services' config.");
                      }

                      nameServerDefaults.put(ns, name);
                   }
                }

                remoteAppServers.add(data);
             }
          }

          // last use of the directory, end our session with it
          ds.unbind();
       }

       /**
        * Obtain an unique ID from the remote side.
        *
        * @return   An unique ID.
        */
       public synchronized static int uniqueId()
       {
          do
          {
             nextId = nextId + 1;
             if (nextId == Integer.MAX_VALUE)
             {
                nextId = 0;
             }

             synchronized (lock)
             {
                if (!requests.containsKey(nextId) && !stoppedRequests.contains(nextId))
                {
                   break;
                }
             }
          }
          while (true);

          return nextId;
       }

       /**
        * Calculate a random string, to be used as a connection ID for multi-session agent appserver, or as a
        * context ID for {@link com.goldencode.p2j.oo.lang.OerequestInfo}.
        *
        * @param    underscore
        *           Flag indicating if the 14 char is an underscore.
        *
        * @return   A random string.
        */
       static String nextContextId(boolean underscore)
       {
          // 13 8bit chars, underscore (optional), 8bit chars

          String id = "";
          byte[] bytes = new byte[21 + (underscore ? 0 : 1)];
          new Random().nextBytes(bytes);
          for (int i = 0; i < bytes.length; i++)
          {
             // make positive, modulo by 52
             int k = bytes[i];
             k = (k + Byte.MAX_VALUE) % 52;

             char next = (char) ('a' + (k % 26));
             if (k >= 26)
             {
                next = Character.toUpperCase(next);
             }
             id = id + next;
             if (underscore && i == 12)
             {
                id = id + "_";
             }
          }

          return id;
       }

       /**
        * Obtain a proxy for the given external procedure. This will also reserve an agent which will
        * be used for all subsequent invocation (including external procedure initialization).
        *
        * @param    id
        *           The appserver connection id.
        * @param    name
        *           A name of a remote external procedure.
        *
        * @return   A result containing the details about this proxy procedure (resource ID, agent ID).
        */
       public static AppServerInvocationResult obtainProxy(String id, String name)
       {
          AppServerInvocationResult res = new AppServerInvocationResult();

          handle hProc = new handle();

          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             msaManager.obtainProxy(hProc, res, id, name);
          }
          else
          {
             try
             {
                obtainProxy(hProc, res, id, name);
             }
             catch (NumberedException ignored)
             {
                return null;
             }
          }

          ExternalProgramWrapper extProg = (ExternalProgramWrapper) hProc.getResource();
          if (extProg != null && extProg.valid())
          {
             String code = new handle(extProg).toString();
             res.setProcedureId(code);
          }
          else
          {
             res.setInvalidInHandle(true);
          }
          return res;
       }

       /**
        * Obtain a proxy for the given external procedure. This will also reserve an agent which will
        * be used for all subsequent invocation (including external procedure initialization).
        *
        * @param    hProc
        *           The procedure handle.
        * @param    res
        *           The invocation result container.
        * @param    id
        *           The appserver connection id.
        * @param    name
        *           A name of a remote external procedure.
        */
       private static void obtainProxy(handle hProc, AppServerInvocationResult res, String id, String name)
       throws NumberedException
       {
          AgentPool pool = getAgentPool(id);

          // acquire a free agent
          Agent agent = null;

          try
          {
             agent = pool.acquireAgent(id);
             agent.obtainProxy(hProc, res, id, name);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Initialize the specified proxy procedure, by calling the <code>execute</code> method.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    code
        *           The code of a persistent external procedure to which the agent is associated. May
        *           be <code>null</code>.
        * @param    agentId
        *           The agent ID where this remote procedure was created.
        * @param    transactionDistinct
        *           Flag indicating if the TRANSACTION DISTINCT clause is in effect.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        *
        * @return   The result of the external program initialization.
        */
       public static AppServerInvocationResult initializeProxy(String   id,
                                                               int      requestId,
                                                               String   code,
                                                               int      agentId,
                                                               boolean  transactionDistinct,
                                                               String   modes,
                                                               Object[] args)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.initializeProxy(id, requestId, code, agentId, transactionDistinct, modes, args);
          }

          AgentPool pool = getAgentPool(id);

          Agent agent;
          try
          {
             agent = pool.acquireAgent(id, code, agentId);
          }
          catch (NumberedException e)
          {
             AppServerInvocationResult result = new AppServerInvocationResult();
             result.setError(e);
             return result;
          }

          try
          {
             // all internal-entries must use the same agent, if this is async and session-free!!!
             return agent.initializeProxy(id, code, requestId, transactionDistinct, modes, args);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Send the STOP condition to the request with the specified ID.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        */
       public static void sendStop(String id, int requestId)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             msaManager.sendStop(id);
             return;
          }

          // security check
          getAgentPool(id);

          synchronized (lock)
          {
             Agent a = requests.get(requestId);
             if (a == null)
             {
                // this means the agent didn't get a chance to start, collect this as a stopped
                // request
                stoppedRequests.add(requestId);
             }
             else
             {
                // terminate the agent
                a.stop();
             }
          }
       }

       /**
        * Send a STOP condition to all the requests on the specified connection.
        *
        * @param    id
        *           The appserver connection id.
        */
       public static void sendStop(String id)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             msaManager.sendStop(id);
             return;
          }

          // security check
          getAgentPool(id);

          // get all requests currently processed for this connection ID and terminate them
          synchronized (lock)
          {
             Set<Agent> active = activeAgents.get(id);

             for (Agent a : active)
             {
                a.stop();
             }
          }
       }

       /**
        * Method that specifies the remote procedures the client can execute from the AppServer
        * session.
        *
        * @param    procList
        *           The list of procedure  and name-patterns separated by comma.
        *
        * @return   <code>true</code> if the method succeeded.
        */
       public static logical export(String procList)
       {
          return export(new character(procList));
       }

       /**
        * Method that specifies the remote procedures the client can execute from the AppServer
        * session.
        *
        * @param    procList
        *           The list of procedure  and name-patterns separated by comma.
        *
        * @return   <code>true</code> if the method succeeded.
        */
       public static logical export(character procList)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaContext())
          {
             return msaManager.export(procList);
          }

          AgentPool pool = getAgentPoolBySubject(SecurityManager.getInstance().getUserId());
          if (pool == null)
          {
             // not an AppServer
             return new logical(false);
          }
          pool.setExports(procList);

          return new logical(true);
       }

       /**
        * Method implementation for getting the SERVER-OPERATING-MODE attribute of the session handle.
        * <p>
        * Possible values:
        * <ul>
        *    <li>State-reset
        *    <li>State-aware
        *    <li>Stateless
        *    <li>State-free
        * </ul>
        *
        * @return   The corresponding SERVER-OPERATING-MODE attribute of the session handle.
        */
       public static character getServerOperatingMode()
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaContext())
          {
             return msaManager.isSessionFree() ?
                AppServerSessionModel.SESSION_FREE.toChar() :
                AppServerSessionModel.SESSION_MANAGED.toChar();
          }

          AgentPool pool = getAgentPoolBySubject(SecurityManager.getInstance().getUserId());
          if (pool == null)
          {
             return new character("");
          }

          String state = pool.getOperatingMode().toString();
          if (pool.isMultiSession())
          {
             state = pool.getOperatingMode().isSessionFree() ?
                AppServerSessionModel.SESSION_FREE.toString() :
                AppServerSessionModel.SESSION_MANAGED.toString();
          }

          return new character(state);
       }

       /**
        * Check if the specified web service token is currently in use (there is an active API call on an agent).
        *
        * @param    token
        *           The web service token.
        *
        * @return   <code>true</code> if {@link Agent#activeWebServiceTokens} contains this token.
        */
       public static boolean isWebServiceTokenInUse(String token)
       {
          return Agent.isWebServiceTokenInUse(token);
       }

       /**
        * Indicates if the session is an AppServer session or not.
        *
        * @return   <code>true</code> if the current session is an AppServer session or false
        *           otherwise.
        */
       public static boolean isRemote()
       {
          return MultiSessionAppserverManager.getInstance().isMsaContext() || local.get().appServer != null;
       }

       /**
        * Check if this FWD context is running an Agent under MSA compatibility.
        *
        * @return   See above.
        */
       public static boolean isMultiSession()
       {
          return MultiSessionAppserverManager.getInstance().isMsaContext() ||
             (isRemote() && local.get().agent.isMultiSession());
       }

       /**
        * Get the SERVER-CONNECTION-BOUND attribute of this session.
        *
        * @return   See above.
        */
       public static logical isServerConnectionBound()
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaContext())
          {
             return new logical(msaManager.isConnectionBound());
          }

          WorkArea wa = local.get();
          return wa.appServer == null ? new logical() : new logical(wa.agent.isBound());
       }

       /**
        * Get the SERVER-CONNECTION-BOUND-REQUEST attribute of this session.
        *
        * @return   See above.
        */
       public static logical isServerConnectionBoundRequest()
       {
          if (MultiSessionAppserverManager.getInstance().isMsaContext())
          {
             return new logical(MultiSessionAppserverManager.isServerConnectionBoundRequest());
          }

          WorkArea wa = local.get();
          if (wa.appServer == null || wa.agent.getOperatingMode() != AppServerOperatingMode.STATELESS)
          {
             return new logical(false);
          }

          return new logical(wa.agent.isBound());
       }

       /**
        * Set the SERVER-CONNECTION-BOUND-REQUEST attribute of this session.
        *
        * @param   bind
        *          The bound state.
        */
       public static void setServerConnectionBoundRequest(boolean bind)
       {
          if (MultiSessionAppserverManager.getInstance().isMsaContext())
          {
             MultiSessionAppserverManager.setServerConnectionBoundRequest(bind);
             return;
          }

          WorkArea wa = local.get();
          if (wa.appServer == null)
          {
             // do nothing
             return;
          }

          AppServerOperatingMode mode = wa.agent.getOperatingMode();
          if (mode != AppServerOperatingMode.STATELESS)
          {
             ErrorManager.recordOrShowError(10849, mode.toString() + " operating mode",
                                            false, false, false, false);
             return;
          }

          if (bind)
          {
             wa.agent.bind();
          }
          else
          {
             wa.agent.unbind();
          }
       }

       /**
        * Get the SERVER-CONNECTION-CONTEXT attribute of this session.
        *
        * @return   See above.
        */
       public static String getServerConnectionContext()
       {
          if (MultiSessionAppserverManager.getInstance().isMsaContext())
          {
             return MultiSessionAppserverManager.getServerConnectionContext();
          }

          WorkArea wa = local.get();

          if (wa.appServer == null)
          {
             return "";
          }

          return wa.agent.getServerConnectionContext(wa.connectionId);
       }

       /**
        * Set the SERVER-CONNECTION-CONTEXT attribute of this session.
        *
        * @param    value
        *           The attribute's value.
        */
       public static void setServerConnectionContext(String value)
       {
          if (MultiSessionAppserverManager.getInstance().isMsaContext())
          {
             MultiSessionAppserverManager.setServerConnectionContext(value);
             return;
          }

          WorkArea wa = local.get();

          if (wa.appServer == null)
          {
             return;
          }

          // for State-free mode, documentation states that a WARNING is raised, but there is no such
          // proof of this during testing... the value can be read after it was set.
          wa.agent.setServerConnectionContext(wa.connectionId, value);
       }

       /**
        * Get the value of SESSION:SERVER-CONNECTION-ID attribute.
        *
        * @return  the value of SESSION:SERVER-CONNECTION-ID attribute.
        */
       public static String getServerConnectionId()
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaContext())
          {
             return msaManager.getConnectionId();
          }
          WorkArea wa = local.get();
          return wa.appServer == null ? "" : wa.connectionId;
       }

       /**
        * Get the name of the remote procedure, specified by the resource ID.
        *
        * @param    id
        *           The appserver connection id.
        * @param    resId
        *           The ID of a remote procedure.
        * @param    agentId
        *           The agent ID where this remote procedure was created.
        *
        * @return   The procedure's name.
        *
        * @see      #getAgentPool(String)
        */
       public static character getProcedureName(String id, String resId, int agentId)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();

          String procedureName;
          if (msaManager.isMsaConnection(id))
          {
             procedureName = msaManager.getProcedureName(id, resId, agentId);
          }
          else
          {
             AgentPool pool = getAgentPool(id);
             Agent agent = pool.getAgent(agentId);
             procedureName = agent.getProcedureName(resId);
          }

          return procedureName == null ? new character() : new character(procedureName);
       }

       /**
        * Delete the remote procedure, specified by the resource id. This must be called at the same
        * time when the local proxy procedure is deleted.
        *
        * @param    id
        *           The appserver connection id.
        * @param    resId
        *           The ID of a remote procedure which needs to be deleted.
        * @param    agentId
        *           The agent ID where this remote procedure was created.
        * @param    token
        *           When not null, it represents the token of a FWD context created for an authenticated and
        *           authorized web service call.  The API call will be performed in that context.
        *
        * @return   <code>true</code> if the remote procedure was deleted.
        *
        * @see      #getAgentPool(String)
        */
       public static boolean deleteProcedure(String id, String resId, int agentId, String token)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.deleteProcedure(id, resId, agentId);
          }

          AgentPool pool = getAgentPool(id);
          Agent agent = pool.getAgent(agentId);

          if (agent == null)
          {
             // agent ID unknown
             return false;
          }

          if (!agent.hasProcedure(resId))
          {
             // not the agent's procedure
             return false;
          }

          // this needs to be done using the agent's context
          agent.deleteProcedure(id, resId, token);

          return true;
       }

       /**
        * Delete the remote object, specified by the resource ID. This must be called at the same
        * time when the local proxy object is deleted.
        *
        * @param    id
        *           The appserver connection id.
        * @param    resId
        *           The ID of a remote object which needs to be deleted.
        * @param    agentId
        *           The agent ID where this remote procedure was created.
        *
        * @return   <code>true</code> if the remote object was deleted.
        */
       public static boolean deleteObject(String id, String resId, int agentId)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.deleteObject(id, resId, agentId);
          }

          AgentPool pool = getAgentPool(id);
          Agent agent = pool.getAgent(agentId);

          if (agent == null)
          {
             // agent ID unknown
             return false;
          }

          if (!agent.hasProcedure(resId))
          {
             // not the agent's procedure
             return false;
          }

          // this needs to be done using the agent's context
          agent.deleteObject(id, resId);

          return true;
       }

       /**
        * Establish a new appserver connection. This will invoke any connect procedure for this
        * appserver, to which the given <code>user</code>, <code>pwd</code> and <code>serverInfo</code>
        * parameters will be passed.
        * <p>
        * The appserver connection must be configured to start under the given P2J-level subject ID.
        *
        * @param    appServer
        *           The name of the target appserver.
        * @param    sessionFree
        *           Flag indicating if the connection request is for a session-free operating mode.
        * @param    user
        *           The first parameter to pass to the connect procedure, if any.
        * @param    pwd
        *           The second parameter to pass to the connect procedure, if any.
        * @param    serverInfo
        *           The third parameter to pass to the connect procedure, if any.
        * @param    requestInfo
        *           The remote {@link com.goldencode.p2j.oo.lang.OerequestInfo} details, as set by
        *           {@link com.goldencode.p2j.oo.lang.OerequestInfo#toArray()}.
        *
        * @return   An {@link AppServerInvocationResult} instance with any result or caught errors.
        */
       public static AppServerInvocationResult connect(String   appServer,
                                                       boolean  sessionFree,
                                                       String   user,
                                                       String   pwd,
                                                       String   serverInfo,
                                                       Object[] requestInfo)
       {
          return connect(appServer, sessionFree, user, pwd, serverInfo, requestInfo, null);
       }

       /**
        * Establish a new appserver connection. This will invoke any connect procedure for this
        * appserver, to which the given <code>user</code>, <code>pwd</code> and <code>serverInfo</code>
        * parameters will be passed.
        * <p>
        * The appserver connection must be configured to start under the given P2J-level subject ID.
        *
        * @param    appServer
        *           The name of the target appserver.
        * @param    sessionFree
        *           Flag indicating if the connection request is for a session-free operating mode.
        * @param    user
        *           The first parameter to pass to the connect procedure, if any.
        * @param    pwd
        *           The second parameter to pass to the connect procedure, if any.
        * @param    serverInfo
        *           The third parameter to pass to the connect procedure, if any.
        * @param    requestInfo
        *           The remote {@link com.goldencode.p2j.oo.lang.OerequestInfo} details, as set by
        *           {@link com.goldencode.p2j.oo.lang.OerequestInfo#toArray()}.
        * @param    webServiceType
        *           The type of web service the worker is used for.
        *
        * @return   An {@link AppServerInvocationResult} instance with any result or caught errors.
        */
       public static AppServerInvocationResult connect(String   appServer,
                                                       boolean  sessionFree,
                                                       String   user,
                                                       String   pwd,
                                                       String   serverInfo,
                                                       Object[] requestInfo,
                                                       String   webServiceType)
       {
          if (MultiSessionAppserverDefinition.get(appServer) != null)
          {
             return MultiSessionAppserverManager.getInstance().connect(appServer,
                                                                       sessionFree,
                                                                       user,
                                                                       pwd,
                                                                       serverInfo,
                                                                       requestInfo,
                                                                       webServiceType);
          }

          final AgentPool pool;
          synchronized (appServers)
          {
             pool = appServers.get(appServer);
          }

          AppServerInvocationResult result = null;

          if (pool == null)
          {
             result = new AppServerInvocationResult();

             result.setError(new RuntimeException("No appserver named " + appServer + " exists!"));

             return result;
          }

          // latch to mark when the session listener can process
          final CountDownLatch connectDone = new CountDownLatch(1);

          // special var to track session disconnected state
          final AtomicBoolean sessionTerminated = new AtomicBoolean(false);

          // the connect ID - use an array to expose it to the session listener
          final String[] connectId = new String[1];

          // the session listener needs to be registered early, in case the the session is terminated
          // while establishing the appserver connection
          SessionListener listener = new SessionListener()
          {
             /**
              * This callback will be called when the P2J client session is ending.
              *
              * @param    session
              *           The session that is ending.
              */
             @Override
             public void terminate(Session session)
             {
                // notify the session has been terminated
                sessionTerminated.set(true);

                // wait for the connect procedure to process, before going forward
                try
                {
                   connectDone.await();
                }
                catch (InterruptedException e)
                {
                   // no-op
                }

                // 1. the connection was not established: thus nothing to do, as the agent should be
                //    properly disconnect at this time.
                if (connectId[0] == null)
                {
                   return;
                }

                // 2. the connection was established, thus disconnect might be needed
                SecurityManager sm = SecurityManager.getInstance();
                synchronized (authorizedSessions)
                {
                   Set<Integer> sessions = authorizedSessions.get(connectId[0]);

                   // only if we are the last authorized session on this connection
                   if (sessions != null                     &&
                       sessions.contains(sm.sessionSm.getSessionId()) &&
                       sessions.size() == 1)
                   {
                      disconnect(connectId[0]);
                   }
                }

                synchronized (lock)
                {
                   activeAgents.remove(connectId[0]);
                }
             }

             /**
              * This callback will be called after the security context has been set up.
              *
              * @param    session
              *           The session that is starting.
              */
             @Override
             public void initialize(Session session)
             {
                // no ops
             }
          };

          final Session session = SessionManager.get().getSession();

          // the session listener registration needs to protected, so that the queue can't be stopped
          // while adding it.  This can be done only by obtaining a lock on the Queue object associated
          // with the session, and is done by the specialized API in the Session class.

          if (session != null && !session.addSessionListener(listener, true))
          {
             result = new AppServerInvocationResult();

             result.setError(new RuntimeException("Client session disconnected before the connect" +
                                                  "procedure was started."));

             return result;
          }

          // WARNING! From this point forward, no synchronized Queue APIs can be called, else a
          // deadlock might occur.  Also, no other listeners can be added for the same reason.
          try
          {
             result = pool.connect(sessionTerminated,
                                   appServer,
                                   sessionFree,
                                   user,
                                   pwd,
                                   serverInfo,
                                   requestInfo);
          }
          finally
          {
             if (result != null && result.getError() == null)
             {
                connectId[0] = result.getConnectionID();

                if (connectId[0] != null)
                {
                   sessionListeners.put(connectId[0], listener);

                   // only if the client socket is still alive

                   SecurityManager sm = SecurityManager.getInstance();

                   // add this context to the authorized connection IDs
                   synchronized (authorizedSessions)
                   {
                      Set<Integer> sessions = authorizedSessions.get(connectId[0]);
                      if (sessions == null)
                      {
                         authorizedSessions.put(connectId[0], sessions = new HashSet<Integer>());
                      }

                      sessions.add(sm.sessionSm.getSessionId());
                   }

                   synchronized (lock)
                   {
                      activeAgents.put(connectId[0], new HashSet<>());
                   }
                }
             }

             // notify the session listener that now is OK to process
             connectDone.countDown();
          }

          return result;
       }

       /**
        * Disconnect the appserver connection, with the given ID.  It will invoke any configured
        * disconnect procedure, but will not return any errors or values to the caller.
        *
        * @param    id
        *           The connection ID to disconnect.
        *
        * @see      #getAgentPool(String)
        */
       public static void disconnect(String id)
       {
          disconnect(null, id);
       }

       /**
        * Disconnect the appserver connection, with the given ID.  It will invoke any configured
        * disconnect procedure, but will not return any errors or values to the caller.
        *
        * @param    pool
        *           The pool to which the connection belongs (may be <code>null</code>, in which case
        *           {@link #getAgentPool} is used to resolve it).
        * @param    id
        *           The connection ID to disconnect.
        *
        * @see      #getAgentPool(String)
        */
       public static void disconnect(AgentPool pool, String id)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             msaManager.disconnect(id, false);
             return;
          }

          if (!connected(id))
          {
             return;
          }

          if (pool == null)
          {
             pool = getAgentPool(id);
          }

          try
          {
             pool.disconnect(id);
          }
          finally
          {
             SecurityManager sm = SecurityManager.getInstance();
             synchronized (authorizedSessions)
             {
                Set<Integer> sessions = authorizedSessions.get(id);
                sessions.remove(sm.sessionSm.getSessionId());

                if (sessions.isEmpty())
                {
                   authorizedSessions.remove(id);
                }
             }

             SessionListener listener = sessionListeners.remove(id);
             if (listener != null)
             {
                Session session = SessionManager.get().getSession();
                session.removeSessionListener(listener);
             }
          }
       }

       /**
        * Create a new instance of the specified qualified class name.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    name
        *           The legacy qualified class name for the legacy class.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The constructor's arguments.
        *
        * @return   A {@link AppServerInvocationResult} instance, which holds the code for the remote
        *           object, all updated OUTPUT/INPUT-OUTPUT arguments and any returned value.
        */
       public static AppServerInvocationResult newInstance(String    id,
                                                           int       requestId,
                                                           long      timeout,
                                                           character name,
                                                           String    modes,
                                                           Object... args)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.newInstance(id, requestId, timeout, name, modes, args);
          }

          AgentPool pool = getAgentPool(id);

          // get the agent from the pool, depending on the operating mode
          // ensure all internal-entries must use the same agent, if this is async and session-free
          Agent agent;
          try
          {
             agent = pool.acquireAgent(id);
          }
          catch (NumberedException e)
          {
             AppServerInvocationResult result = new AppServerInvocationResult();
             result.setError(e);
             return result;
          }

          try
          {
             // send the invoke command to the agent
             return agent.newInstance(id, requestId, timeout, name, modes, args);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Invoke a method in a remotely instantiated class.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    returnValue
        *           Flag indicating if the return value is required.
        * @param    name
        *           The legacy 4GL name for the class method.
        * @param    resId
        *           The code of a remotely instantiated object in which to invoke the method.
        * @param    agentId
        *           The agent ID where this remote procedure was created.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The method's arguments.
        *
        * @return    A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT
        *            arguments and any returned value.
        */
       public static AppServerInvocationResult invokeMethod(String    id,
                                                            int       requestId,
                                                            long      timeout,
                                                            boolean   returnValue,
                                                            character name,
                                                            String    resId,
                                                            int       agentId,
                                                            String    modes,
                                                            Object... args)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.invokeMethod(id, requestId, timeout, returnValue, name, resId, agentId, modes, args);
          }

          AgentPool pool = getAgentPool(id);

          // get the agent from the pool, depending on the operating mode
          // ensure all internal-entries must use the same agent, if this is async and session-free
          Agent agent;
          try
          {
             agent = pool.acquireAgent(id, resId, agentId);
          }
          catch (NumberedException e)
          {
             AppServerInvocationResult result = new AppServerInvocationResult();
             result.setError(e);
             return result;
          }

          try
          {
             // send the invoke command to the agent
             return agent.invokeMethod(id, requestId, timeout, returnValue, name, resId, modes, args);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Invoke a remote external procedure, internal procedure or user-defined function. The
        * validation for the program name and arguments will be done on the remote side.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    name
        *           The legacy 4GL name for the procedure/function.
        * @param    procID
        *           The code of a remote procedure in which to invoke the procedure or function.
        * @param    agentId
        *           The agent ID where this remote procedure was created.
        * @param    function
        *           <code>true</code> if this is a function call.
        * @param    dynamicFunction
        *           <code>true</code> if this is a DYNAMIC-FUNCTION call.
        * @param    superCall
        *           <code>true</code> if this is a RUN SUPER or SUPER() call.
        * @param    transactionDistinct
        *           Flag indicating if the TRANSACTION DISTINCT clause is in effect.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        *
        * @return    A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT
        *            arguments and any returned value.
        *
        * @see      #getAgentPool(String)
        */
       public static AppServerInvocationResult invoke(String    id,
                                                      int       requestId,
                                                      long      timeout,
                                                      character name,
                                                      String    procID,
                                                      int       agentId,
                                                      boolean   function,
                                                      boolean   dynamicFunction,
                                                      boolean   superCall,
                                                      boolean   transactionDistinct,
                                                      String    modes,
                                                      Object... args)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             InvokeConfig cfg = new InvokeConfig();
             cfg.setTarget(name);
             cfg.setDynamicFunction(dynamicFunction);
             cfg.setFunction(function);
             cfg.setSuperCall(superCall);
             cfg.setTransactionDistinct(transactionDistinct);
             cfg.setModes(modes);
             cfg.setArguments(args);

             return msaManager.invoke(id, requestId, cfg, procID, timeout);
          }

          AgentPool pool = getAgentPool(id);

          // get the agent from the pool, depending on the operating mode
          // ensure all internal-entries must use the same agent, if this is async and session-free
          Agent agent;
          try
          {
             agent = pool.acquireAgent(id, procID, agentId);
          }
          catch (NumberedException e)
          {
             AppServerInvocationResult result = new AppServerInvocationResult();
             result.setError(e);
             return result;
          }

          try
          {
             // send the invoke command to the agent
             return agent.invoke(id, requestId, timeout, name, procID,
                                 function, dynamicFunction, superCall,
                                 transactionDistinct, modes, args);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Invoke a remote external procedure, internal procedure or user-defined function. The
        * validation for the program name and arguments will be done on the remote side. Any errors,
        * result or updated arguments will be sent back to the caller.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    cfg
        *           The invoke configuration.
        *
        * @return    A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT
        *            arguments and any returned value.
        */
       public static AppServerInvocationResult invoke(String    id,
                                                      int       requestId,
                                                      long      timeout,
                                                      InvokeConfig cfg)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.invoke(id, requestId, cfg, null, timeout);
          }

          AgentPool pool = getAgentPool(id);
          // get the agent from the pool, depending on the operating mode
          // ensure all internal-entries must use the same agent, if this is async and session-free
          Agent agent;
          try
          {
             String procID = null;
             int agentId = -1;
             handle inHandle = cfg.getInHandle();
             if (ProcedureManager.isProcedure(inHandle))
             {
                if (ProcedureManager.isProxy(inHandle))
                {
                   procID = ((ProxyProcedureWrapper) inHandle.getResource()).getResourceId();
                   agentId = ((ProxyProcedureWrapper) inHandle.getResource()).getAgentId();
                }
                else if (!(inHandle.getResource() instanceof DeferredProgramWrapper))
                {
                   // a deferred program means that it is either SINGLETON or SINGLE-RUN, and will be instantiated
                   // on the remote side - so no procedure ID can be acquired.

                   procID = inHandle.toString();
                }
             }

             agent = pool.acquireAgent(id, procID, agentId);
          }
          catch (NumberedException e)
          {
             AppServerInvocationResult result = new AppServerInvocationResult();
             result.setError(e);
             return result;
          }

          try
          {
             // send the invoke command to the agent
             return agent.invoke(id, requestId, timeout, cfg);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Invoke a remote external procedure, persistently. The validation of the program name and
        * arguments will be done on the remote side.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    name
        *           The legacy 4GL name for the procedure.
        * @param    transactionDistinct
        *           Flag indicating if the TRANSACTION DISTINCT clause is in effect.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        *
        * @return    A {@link AppServerInvocationResult} instance, which holds the code for the remote
        *            procedure, all updated OUTPUT/INPUT-OUTPUT arguments and any returned value.
        *
        * @see      #getAgentPool(String)
        */
       public static AppServerInvocationResult invokePersistent(String    id,
                                                                int       requestId,
                                                                long      timeout,
                                                                character name,
                                                                boolean   transactionDistinct,
                                                                String    modes,
                                                                Object... args)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {

             InvokeConfig cfg = new InvokeConfig();
             cfg.setArguments(args);
             cfg.setModes(modes);
             cfg.setProcedureHandle(new handle());
             cfg.setTarget(name);
             cfg.setTransactionDistinct(transactionDistinct);

             return msaManager.invoke(id, requestId, cfg, null, timeout);
          }

          AgentPool pool = getAgentPool(id);

          // get the agent from the pool, depending on the operating mode
          // ensure all internal-entries must use the same agent, if this is async and session-free
          Agent agent;
          try
          {
             agent = pool.acquireAgent(id);
          }
          catch (NumberedException e)
          {
             AppServerInvocationResult result = new AppServerInvocationResult();
             result.setError(e);
             return result;
          }

          try
          {
             // send the invoke command to the agent
             return agent.invokePersistent(id, requestId, timeout, name, transactionDistinct, modes, args);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Handle a generic web service request.  This exposes the Jetty request and response objects
        * to the Agent which will process it, so it is required for the appserver to be local.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    handler
        *           The {@link com.goldencode.p2j.oo.web.WebHandler} implementation.
        * @param    basepath
        *           The service basepath.
        * @param    paths
        *           The split paths for this handler.
        * @param    token
        *           When not null, it represents the token of a FWD context created for an authenticated and
        *           authorized web service call.  The API call will be performed in that context.
        * @param    target
        *           The request original target.
        * @param    request
        *           The Jetty request.
        * @param    response
        *           The Jetty response.
        *
        * @return   A {@link AppServerInvocationResult} instance with any error throw during the web service
        *           processing.
        */
       public static AppServerInvocationResult handleWebService(String                      id,
                                                                int                         requestId,
                                                                long                        timeout,
                                                                Class<? extends WebHandler> handler,
                                                                String                      basepath,
                                                                String[]                    paths,
                                                                String                      token,
                                                                String                      target,
                                                                HttpServletRequest          request,
                                                                HttpServletResponse         response)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.handleWebService(id, requestId, timeout, handler,
                                        basepath, paths, token, target, request, response);
          }

          AgentPool pool = getAgentPool(id);
          AppServerInvocationResult result = new AppServerInvocationResult();

          // get the agent from the pool, depending on the operating mode
          Agent agent;
          try
          {
             agent = pool.acquireAgent(id);
          }
          catch (NumberedException e)
          {
             result.setError(e);
             return result;
          }

          try
          {
             // send the invoke command to the agent
             return agent.handleWebService(id, requestId, timeout, handler,
                                           basepath, paths, token, target, request, response);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Invoke the given Java method directly.
        * <p>
        * All arguments are sent by references: if it was marked as an OUTPUT or INPUT-OUTPUT, any change to that
        * instance will be reflected back to the caller.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    cfg
        *           The configuration with the Java method to be invoked.
        *
        * @return   A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT
        *           arguments and any returned value.
        */
       public static AppServerInvocationResult invoke(String           id,
                                                      int              requestId,
                                                      int              timeout,
                                                      JavaInvokeConfig cfg)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.invokeJavaMethod(id, requestId, timeout, cfg);
          }

          AgentPool pool = getAgentPool(id);

          int agentId = -1;

          Agent agent;
          try
          {
             agent = pool.acquireAgent(id, null, agentId);
          }
          catch (NumberedException e)
          {
             AppServerInvocationResult result = new AppServerInvocationResult();
             result.setError(e);
             return result;
          }

          try
          {
             // send the invoke command to the agent
             return agent.invoke(id, requestId, timeout, cfg);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Perform a pseudo-dynamic call where the target procedure doesn't receive the parameter as arguments at
        * the Java method definition, but instead they are managed via {@link LegacyOpenClientCaller} APIs.
        * <p>
        * This allows the target program to act as a controller, where it can prepare the arguments, perform
        * security checks, etc, before dispatching the call to the real target, which can be resolved from the
        * arguments or in some other way.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    procedure
        *           The target external program.  Must have no parameters defined.
        * @param    argTypes
        *           The argument types, one of the e.g. {@link LegacyJavaAppserverApi#integer()} values.
        * @param    modes
        *           The argument modes, one of the (I)NPUT, (O)UTPUT or INP(U)T-OUTPUT values.
        * @param    args
        *           The actual arguments.
        *
        * @return   A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT arguments
        *           and any returned value.
        */
       public static AppServerInvocationResult invokeWithArgs(String    id,
                                                              int       requestId,
                                                              long      timeout,
                                                              String    procedure,
                                                              int[]     argTypes,
                                                              String    modes,
                                                              Object... args)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             return msaManager.invokeWithArgs(id, requestId, timeout, procedure, argTypes, modes, args);
          }

          AgentPool pool = getAgentPool(id);

          int agentId = -1;

          Agent agent;
          try
          {
             agent = pool.acquireAgent(id, null, agentId);
          }
          catch (NumberedException e)
          {
             AppServerInvocationResult result = new AppServerInvocationResult();
             result.setError(e);
             return result;
          }

          try
          {
             // send the invoke command to the agent
             return agent.invokeWithArgs(id, requestId, timeout, procedure, argTypes, modes, args);
          }
          finally
          {
             pool.releaseAgent(id, agent);
          }
       }

       /**
        * Disable legacy OO errors thrown on this connection.
        *
        * @param    id
        *           The appserver connection id.
        */
       public static void disableLegacyErrors(String id)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaConnection(id))
          {
             msaManager.disableLegacyErrors(id);
             return;
          }

          AgentPool pool = getAgentPool(id);
          pool.disableLegacyErrors(id);
       }

       /**
        * Check if the current {@link WorkArea#connectionId connection ID} should suppress legacy OO errors.
        *
        * @return   See above.
        */
       public static boolean isDisabledLegacyErrors()
       {
          WorkArea wa = local.get();
          AgentPool pool = appServers.get(wa.appServer);
          if (pool != null)
          {
             return pool.isDisabledLegacyErrors(wa.connectionId);
          }

          return MultiSessionAppserverManager.getInstance().hasLegacyErrorsDisabled();
       }

       /**
        * Get the number of default threads which can be used on the remote side, to execute appserver calls.
        * This is in sync with the number of maximum agents (or sessions for multi-session mode), being able to
        * process concurrent requests on the FWD server.
        *
        * @param    appserver
        *           The appserver name.
        *
        * @return   See above.
        *
        * @see      RemoteAppServerConnectionPool
        */
       public static int getRemotePoolThreads(String appserver)
       {
          waitForAppserver(appserver);

          // get the number of threads which the remote side can use for executing calls; this number is in sync
          // with the maximum number of agents, which can execute remote calls concurrently, without getting into
          // 'no servers available'; assumption is no agents get bound.
          synchronized (appServers)
          {
             AgentPool pool = appServers.get(appserver);
             return pool.getDef().getMaxAgents();
          }
       }

       /**
        * Wait for the specific appserver to have at least on agent started.
        *
        * @param    appserver
        *           The appserver name.
        */
       public static void waitForAppserver(String appserver)
       {
          // wait for the agent pool to have at least one agent started
          while (true)
          {
             int startedAgents = getNumberOfAgentsInPool(appserver);
             if (startedAgents > 0)
             {
                break;
             }

             try
             {
                Thread.sleep(50);
             }
             catch (InterruptedException e)
             {
                // ignore
             }
          }
       }

       /**
        * Start this FWD context as an appserver agent.  The first established context will initialize the pool
        * associated for this appserver.
        *
        * @param    legacyLogManagerConfigs
        *           Configs for LOG-MANAGER.
        *
        * @return   <code>true</code> if this was an appserver. On <code>false</code>, normal P2J
        *           conversation mode will be started.
        */
       public static boolean startAppServer(LegacyLogManagerConfigs legacyLogManagerConfigs)
       {
          // called on the server-side of the appServer connection

          SecurityManager sm = SecurityManager.getInstance();
          SessionManager sessMgr = SessionManager.get();
          String appServer = sm.getAppServer();

          if (appServer == null)
          {
             if (LOG.isLoggable(Level.FINE))
             {
                Session session = sessMgr.getSession();
                LOG.log(Level.FINE,
                       String.format("No app server for the serverId: %s; %d -> %d", sm.getServerId(),
                                     session.getNodeAddress(), session.getRemoteAddress()));
             }
             // if no appServer defined for this account, then we are a normal client, so fast exit
             return false;
          }

          legacyLogManagerConfigs.setAppServer(appServer);
          LegacyLogOps.appserverNameLogConfigPairs.putIfAbsent(appServer, legacyLogManagerConfigs);

          String subject = SecurityManager.getInstance().getUserId();

          // if this is the first connection of this appServer, it will create the pool.
          // all connections authenticated as this appServer will act as agents
          AgentPool pool = null;
          synchronized (appServers)
          {
             pool = appServers.get(appServer);

             if (pool == null)
             {
                // initialize the pool, if this is the first agent to connect
                pool = AgentPool.newPool(appServer);
                appServers.put(appServer, pool);

                synchronized (subjectToAppserver)
                {
                   subjectToAppserver.put(subject, appServer);
                }
             }
          }

          WorkArea wa = local.get();
          try
          {
             wa.appServer = appServer;

             pool.start();
          }
          finally
          {
             // do not clean up the appserver - let it 'bounce back' if someone else connects to it!
             /*
             if (pool.isEmpty())
             {
                // the app server was terminated, clean up after it
                synchronized (appServers)
                {
                   appServers.remove(pool.getName());
                }

                synchronized (subjectToAppserver)
                {
                   subjectToAppserver.remove(subject);
                }

                AppServerLauncher.terminated(appServer);
             }
             */

             wa.appServer = null;
          }

          return true;
       }

       /**
        * Set the current Agent's connection ID.
        *
        * @param    id
        *           The connection ID.
        */
       static void setConnectionId(String id)
       {
          local.get().connectionId = id;
       }

       /**
        * Check if current running appserver knows the given procedure.
        *
        * @param    referent
        *           A persistent procedure referent.
        *
        * @return   <code>true</code> if the persistent procedure is known to the current running
        *           appserver.
        */
       static boolean hasProcedure(Object referent)
       {
          MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
          if (msaManager.isMsaContext())
          {
             return msaManager.hasProcedure(referent);
          }

          AgentPool pool = getAgentPoolBySubject(SecurityManager.getInstance().getUserId());
          return pool.hasProcedure(referent);
       }

       /**
        * Get the agent pool for the given appserver.
        *
        * @param    appServer
        *           The appserver name.
        *
        * @return   See above.
        */
       public static AgentPool getAgentPoolForAppserver(String appServer)
       {
          synchronized (appServers)
          {
             return appServers.get(appServer);
          }
       }

       /**
        * Get the number of agents started for that appserver.
        *
        * @param    appServer
        *           The appserver name.
        *
        * @return   See above.
        */
       public static int getNumberOfAgentsInPool(String appServer)
       {
          if (MultiSessionAppserverDefinition.get(appServer) != null)
          {
             return MultiSessionAppserverManager.getInstance().getNumberOfAgents(appServer);
          }
          synchronized (appServers)
          {
             AgentPool pool = appServers.get(appServer);
             return pool == null ? 0 : pool.size();
          }
       }

       /**
        * Get the number of ABL sessions started for that appserver.
        *
        * @param    appServer
        *           The appserver name.
        *
        * @return   See above.
        */
       public static int getNumberOfSessions(String appServer)
       {
          if (MultiSessionAppserverDefinition.get(appServer) != null)
          {
             return MultiSessionAppserverManager.getInstance().getNumberOfSessions(appServer);
          }
          synchronized (appServers)
          {
             AgentPool pool = appServers.get(appServer);
             return pool == null ? 0 : pool.size();
          }
       }

       /**
        * Post the specific task to be executed in the current agent's request.
        *
        * @param    work
        *           The task to execute.
        * @param    wait
        *           Flag indicating the method will not return until the task executes; otherwise, the method
        *           returns immediately.
        *
        * @throws   IllegalStateException
        *           If the current context is not one for the agent.
        */
       public static void postTask(Runnable work, boolean wait)
       {
          WorkArea wa = local.get();
          if (!isRemote())
          {
             throw new IllegalStateException("Custom tasks can be posted only from an agent's context!");
          }

          int requestId = uniqueId();
          wa.agent.postTask(work, requestId, wait);
       }

       /**
        * If this is called from an agent's context, check if the agent is still running.
        *
        * @return   See above.
        */
       public static boolean isAgentRunning()
       {
          WorkArea wa = local.get();
          return wa.appServer != null && wa.agent != null && !wa.agent.terminated();
       }

       /**
        * Collect an {@link Agent} as is currently executing a task on the specified connection ID.
        *
        * @param    connectionId
        *           The connection ID.
        * @param    agent
        *           The agent instance.
        */
       static void addActiveAgent(String connectionId, Agent agent)
       {
          if (connectionId != null)
          {
             synchronized (lock)
             {
                if (activeAgents.containsKey(connectionId))
                {
                   activeAgents.get(connectionId).add(agent);
                }
             }
          }

          setConnectionId(connectionId);
       }

       /**
        * Remove the {@link Agent} as it finished executing a task on the specified connection ID.
        *
        * @param    connectionId
        *           The connection ID.
        * @param    agent
        *           The agent instance.
        */
       static void removeActiveAgent(String connectionId, Agent agent)
       {
          if (connectionId != null)
          {
             synchronized (lock)
             {
                if (activeAgents.containsKey(connectionId))
                {
                   activeAgents.get(connectionId).remove(agent);
                }
             }
          }
       }

       /**
        * Associate the specified thread with the specified request, in the {@link #requests} map.
        *
        * @param    requestId
        *           The request ID.
        * @param    a
        *           The agent executing the request.
        *
        * @return   <code>true</code> if a STOP was not already received for this request.
        */
       static boolean registerRequest(int requestId, Agent a)
       {
          synchronized (lock)
          {
             // if a STOP was received for this request, then we don't process it
             if (stoppedRequests.remove(requestId))
             {
                return false;
             }

             // else, register the agent thread for this request
             requests.put(requestId, a);

             return true;
          }
       }

       /**
        * Deregister the specified request from the {@link #requests} map.
        *
        * @param    requestId
        *           The request ID.
        */
       static void deregisterRequest(int requestId)
       {
          synchronized (lock)
          {
             requests.remove(requestId);
          }
       }

       /**
        * Register the specified agent with this FWD context.
        *
        * @param    agent
        *           The agent handling this FWD context.
        */
       static void registerAgent(Agent agent)
       {
          local.get().agent = agent;
       }

       /**
        * Get the agent pool for the given connection ID.
        *
        * @param    connectionId
        *           The connection ID.
        *
        * @return   See above.
        */
       private static AgentPool getAgentPool(String connectionId)
       {
          SecurityManager sm = SecurityManager.getInstance();
          boolean allowed = false;
          synchronized (authorizedSessions)
          {
             Set<Integer> sessions = authorizedSessions.get(connectionId);
             allowed = (sessions != null && sessions.contains(sm.sessionSm.getSessionId()));
          }

          if (!allowed)
          {
             final String msg =
                String.format("Unauthorized usage of connection ID %s from " +
                              "session ID %d by subject %s.",
                              connectionId,
                              sm.sessionSm.getSessionId(),
                              sm.sessionSm.getSessionToken().getSubject());
             if (LOG.isLoggable(Level.WARNING))
             {
                LOG.log(Level.WARNING, msg);
             }

             throw new RuntimeException(msg);
          }

          String appServer = AgentPool.getAppserver(connectionId);
          synchronized (appServers)
          {
             return appServers.get(appServer);
          }
       }

       /**
        * Check if the given connection ID is still connected.
        *
        * @param    connectionId
        *           The connection ID.
        *
        * @return   <code>true</code> if the connection is still alive.
        */
       private static boolean connected(String connectionId)
       {
          String appServer = AgentPool.getAppserver(connectionId);
          return appServer != null;
       }

       /**
        * Search through and determine the remote appserver associated with the given options.
        *
        * @param    appService
        *           The appserver's service name (the -AppService connect parameter).
        * @param    host
        *           The -H connect parameter.
        * @param    serviceName
        *           The network service name, specified by the -S parameter.
        * @param    port
        *           The network port, specified by the -S parameter.
        * @param    directConnect
        *           Flag indicating this is a direct connect (the -DirectConnect parameter).
        * @param    sessionModelString
        *           The session mode: session-free or session-managed.
        *
        * @return   The resolved appserver configuration.
        */
       static RemoteAppServer lookupRemoteAppServer(String  appService,
                                                    String  host,
                                                    String  serviceName,
                                                    int     port,
                                                    boolean directConnect,
                                                    String  sessionModelString)
       {
          // handle the defaults
          if (port == 0 && serviceName == null)
          {
             port = 5162;
          }
          if (host == null)
          {
             host = "localhost";
          }

          List<RemoteAppServer> foundServers = null;
          if (directConnect)
          {
             String expectedHost = host;
             foundServers = remoteAppServers.stream()
                                            .filter(server -> server.hosts.contains(expectedHost))
                                            .collect(Collectors.toList());
             if (foundServers.size() == 0)
             {
                // testcases/appsrv/test/connect_errors/host_name.p
                ErrorManager.recordOrShowError(5482, false, expectedHost);
                ErrorManager.recordOrShowError(5468, false);
                return null;
             }
          }
          else if (appService == null)
          {
             // TODO: what about the default: appService = nameServerDefaults.get(host);
             // testcases/appsrv/test/connect_errors/direct_connect.p
             ErrorManager.recordOrShowError(9407, false, host, String.valueOf(port));
             return null;
          }
          else
          {
             foundServers = remoteAppServers.stream()
                                            .filter(server -> server.aliases.contains(appService))
                                            .collect(Collectors.toList());
             if (foundServers.size() == 0)
             {
                // testcases/appsrv/test/connect_errors/service_name.p
                ErrorManager.recordOrShowError(5484, false, appService);
                ErrorManager.recordOrShowError(5468, false);
                return null;
             }
          }

          if (serviceName != null && !serviceName.isEmpty())
          {
             foundServers = foundServers.stream()
                                        .filter(server -> server.services.contains(serviceName))
                                        .collect(Collectors.toList());
             if (foundServers.isEmpty())
             {
                ErrorManager.recordOrShowError(5484,
                                               false,
                                               "Unknown service " + appService + "  transport TCP.");
                ErrorManager.recordOrShowError(5468, false, "Application server connect failure.");
                return null;
             }
          }

          // TODO: fix the case where the port is 0 in the CONNECT string
          if (port != 0)
          {
             int expectedPort = port;
             foundServers = foundServers.stream()
                                        .filter(server -> server.ports.contains(expectedPort))
                                        .collect(Collectors.toList());
             if (foundServers.size() == 0)
             {
                // testcases/appsrv/test/connect_errors/port_number.p
                ErrorManager.recordOrShowError(9407, false, host, String.valueOf(port));
                ErrorManager.recordOrShowError(5468, false);
                return null;
             }
          }

          RemoteAppServer foundServer = foundServers.get(0);

          if (MultiSessionAppserverDefinition.get(foundServer.name) != null)
          {
             return foundServer;
          }

          boolean isSessionModelDefined = sessionModelString != null && !sessionModelString.isEmpty();
          AppServerSessionModel sessionModel =
             AppServerSessionModel.SESSION_FREE.toString().equalsIgnoreCase(sessionModelString) ?
                AppServerSessionModel.SESSION_FREE :
                AppServerSessionModel.SESSION_MANAGED.toString().equalsIgnoreCase(sessionModelString) ?
                   AppServerSessionModel.SESSION_MANAGED :
                   null;

          if (isSessionModelDefined && sessionModel == null)
          {
             // testcases/appsrv/test/connect_errors/session_model_incorrect.p
             ErrorManager.recordOrShowError(0, "-sessionModel must be either Session-managed or Session-free");
             return null;
          }

          if (foundServer.sessionFree)
          {
             // default or explicit Session-managed
             if (!isSessionModelDefined || sessionModel == AppServerSessionModel.SESSION_MANAGED)
             {
                // testcases/appsrv/test/connect_errors/session_model_mismatch.p
                String errMsg = "Unexpected CONNECT request in state-free mode";
                ControlFlowOps.setReturnValue(errMsg);
                ErrorManager.noRecordOrShowError(errMsg, false);
                ErrorManager.noRecordOrThrowError("Application server connect failure. (5468)");
                return null;
             }
          }
          else
          {
             if (isSessionModelDefined && sessionModel != AppServerSessionModel.SESSION_MANAGED)
             {
                // Stateless / State-aware / State-reset
                ErrorManager.recordOrShowError(13511);
                ErrorManager.recordOrThrowError(5468);
                return null;
             }
          }

          return foundServer;
       }

       /**
        * Get the agent pool for the given subject ID.
        *
        * @param    subjectId
        *           The subject ID.
        *
        * @return   See above.
        */
       private static AgentPool getAgentPoolBySubject(String subjectId)
       {
          String appServer = getAppserverBySubject(subjectId);
          synchronized (appServers)
          {
             return appServers.get(appServer);
          }
       }

       /**
        * Get the appserver name started by the given subject ID.
        *
        * @param    subjectId
        *           The subject ID used to start an appserver.
        *
        * @return   See above.
        */
       private static String getAppserverBySubject(String subjectId)
       {
          synchronized (subjectToAppserver)
          {
             return subjectToAppserver.get(subjectId);
          }
       }

       /**
        * Container for agent-related info.
        */
       private static class WorkArea
       {
          /** The agent handling this FWD context. */
          private Agent agent;

          /**
           * When the P2J client is connected as an appserver, this indicates the appserver's name.
           */
          private String appServer = null;

          /** The Agent's SERVER-CONNECTION-ID value. */
          private String connectionId = null;
       }

       /**
        * Container for the information needed to identify an appserver, by the data configured at the
        * CONNECT statement's options.
        */
       public static class RemoteAppServer
       {
          /** The name of this known appserver. */
          public final String name;

          /** The P2J host identifying this appserver. */
          public final String p2jHost;

          /** The P2J port (secure or insecure). */
          public final int p2jPort;

          /** The account (configured as an appserver) to authenticate on the remote side.*/
          public final String p2jAccount;

          /** The set of aliases defined for this appserver. */
          public final Set<String> aliases = new HashSet<String>();

          /** The list of name servers with which the appserver is registered. */
          public final Set<String> nameServers = new HashSet<String>();

          /** The hosts on which this appserver is running. */
          public final Set<String> hosts = new HashSet<String>();

          /**
           * The ports used by CONNECT statement to target this appserver.
           * May be nameserver or broker ports.
           */
          public final Set<Integer> ports = new HashSet<Integer>();

          /**
           * The network service names used by CONNECT statement to target this appserver.
           */
          public final Set<String> services = new HashSet<String>();

          /** Flag indicating if the appserver is session-free or session-managed. */
          public final boolean sessionFree;

          /**
           * Initialize this remote appserver by reading its configuration from the directory.
           *
           * @param    ds
           *           The {@link DirectoryService} instance.
           * @param    appSrvPath
           *           The directory path where remote appservers are configured.
           * @param    name
           *           The remote appserver's name.
           */
          public RemoteAppServer(DirectoryService ds, String appSrvPath, String name)
          {
             this.name = name;

             String path = appSrvPath + "/" + name;
             this.p2jHost = ds.getNodeString(path + "/p2j_host", "value");
             this.p2jPort = ds.getNodeInteger(path + "/p2j_port", "value");
             this.p2jAccount = ds.getNodeString(path + "/p2j_account", "value");
             Boolean sessionFree = ds.getNodeBoolean(path + "/sessionFree", "value");
             this.sessionFree = sessionFree != null && sessionFree;

             String[] aliases = ds.getNodeStrings(path + "/aliases", "values");
             if (aliases != null)
             {
                this.aliases.addAll(Arrays.asList(aliases));
             }

             String[] nameServers = ds.getNodeStrings(path +"/nameservers", "values");
             if (nameServers != null)
             {
                this.nameServers.addAll(Arrays.asList(nameServers));
             }

             String[] hosts = ds.getNodeStrings(path +"/hosts", "values");
             if (hosts != null)
             {
                this.hosts.addAll(Arrays.asList(hosts));
             }

             Integer[] ports = ds.getNodeIntegers(path +"/ports", "values");
             if (ports != null)
             {
                this.ports.addAll(Arrays.asList(ports));
             }

             String[] services = ds.getNodeStrings(path +"/services", "values");
             if (services != null)
             {
                this.services.addAll(Arrays.asList(services));
             }
          }
       }
    }