# CoreAppserver.java

    /*
    ** Module   : CoreAppserver.java
    ** Abstract : Core appserver reusable logic.
    **
    ** Copyright (c) 2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description----------------------------------
    ** 001 GBB 20250122 Methods moved from Agent to CoreAppserver to be reused by the new MSA appserver.
    */
    /*
    ** 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.appserver;

    import com.goldencode.p2j.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.oo.web.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.trigger.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;

    import javax.servlet.http.*;
    import java.lang.reflect.*;
    import java.util.*;
    import java.util.function.*;
    import java.util.logging.*;

    /** Core appserver reusable logic. */
    public class CoreAppserver
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(CoreAppserver.class);

       /**
        * Initial setup for an appserver session.
        *
        * @param    isServerSideMsa
        *           Flag if the session is for multi-session agent or classic agent.
        * @param    appserverName
        *           The appserver name.
        * @param    searchPath
        *           The search path.
        * @param    topScopeLabel
        *           The label of the top scope.
        */
       public static void prepareSession(boolean isServerSideMsa,
                                         String appserverName,
                                         String searchPath,
                                         String topScopeLabel)
       {
          // set this as batch mode
          EnvironmentOps.setBatchMode(true);

          if (!isServerSideMsa)
          {
             LogicalTerminal.activateBatchMode(true);
          }

          // set the propath
          EnvironmentOps.setSearchPath(searchPath);

          // force DatabaseTriggerManager context-local instantiation
          DatabaseTriggerManager.get();

          ProcedureManager.load();

          try
          {
             LegacyLogManagerConfigs legacyLogManagerConfigs =
                LegacyLogOps.appserverNameLogConfigPairs.get(appserverName);
             LegacyLogOps.logMgr().initialize(legacyLogManagerConfigs);
          }
          catch (Throwable t)
          {
             LOG.log(Level.WARNING, t, "LOG-MANAGER couldn't be initialized for appserver %s.", appserverName);
          }

          // Global scope for keeping buffers states (like BufferManager.allBuffers). This scope must be
          // explicitly popped when the agent context gets reset or when the agent terminates, so the dynamic
          // DMOs and any other global finalizables must be processed.
          TransactionManager.pushScope(topScopeLabel,
                                       TransactionManager.NO_TRANSACTION,
                                       true,
                                       true,
                                       false,
                                       false);

          if (!isServerSideMsa)
          {
             // always null stream
             UnnamedStreams.assignOut(StreamFactory.openNullStream());
          }

          DatabaseManager.autoConnect();
       }

       /**
        * 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    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    isMultiSession
        *           Flag to indicate running in a multi-session agent appserver.
        * @param    isNewMultiSession
        *           Flag to indicate running in the new version of the multi-session agent appserver.
        * @param    exports
        *           The export list for remote procedures the client can execute in the current session.
        * @param    activateProc
        *           The session activate procedure.
        * @param    deactivateProc
        *           The session deactivate procedure.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    connectionId
        *           The appserver connection id.
        * @param    agentId
        *           The agent id.
        * @param    threadId
        *           The thread id.
        * @param    sessionId
        *           The appserver session id.
        * @param    cfg
        *           The invoke configuration.
        * @param    inProcResId
        *           The resource id of the procedure.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    removeProcedureFunc
        *           The function removing the procedure.
        * @param    disconnectFunc
        *           The function disconnecting the client.
        */
       public static void invoke(AppServerInvocationResult invocationResult,
                                 boolean isMultiSession,
                                 boolean isNewMultiSession,
                                 character exports,
                                 String activateProc,
                                 String deactivateProc,
                                 Supplier<Boolean> isBoundSupplier,
                                 String connectionId,
                                 int agentId,
                                 Integer threadId,
                                 Integer sessionId,
                                 InvokeConfig cfg,
                                 String inProcResId,
                                 BiConsumer<String, String> setRunningProcFunc,
                                 BiConsumer<String, String> removeProcedureFunc,
                                 Consumer<String> disconnectFunc)
       {
          setOeRequestInfo(cfg, isMultiSession, agentId, threadId, sessionId);

          if (!invokeActivate(activateProc, deactivateProc, isBoundSupplier, connectionId, invocationResult,
                              setRunningProcFunc, disconnectFunc))
          {
             return;
          }

          // resolve the procedure
          handle inHandle = cfg.getInHandle();
          String resId = null;
          String runningProgram = null;
          if (cfg.isSingleRun() || cfg.isSingleton())
          {
             DeferredProgram deferred = (DeferredProgram) inHandle.get();
             deferred.setSingleRun(cfg.isSingleRun());
             deferred.setSingleton(cfg.isSingleton());

             try
             {
                inHandle = (handle) executeScoped(() -> ControlFlowOps.resolveInstance(deferred, cfg.isClass()));
             }
             catch (ConditionException exc)
             {
                invocationResult.setError(exc);
             }
             catch (Throwable t)
             {
                LOG.severe("", t);
                invocationResult.setError(t);
             }

             if (inHandle == null || !inHandle._isValid())
             {
                return;
             }

             runningProgram = deferred.getName();
          }
          else if (inHandle != null || inProcResId != null)
          {
             resId = inProcResId != null ?
                inProcResId :
                ((ProxyProcedureWrapper) inHandle.getResource()).getResourceId();
             // resolve the procedure - this is actually 'extProg', validated when the agent was acquired.
             inHandle = handle.fromString(resId);

             runningProgram = resId;
          }

          runningProgram = runningProgram == null ? cfg.getTarget() : runningProgram + " / " + cfg.getTarget();
          setRunningProcFunc.accept(connectionId, runningProgram);

          boolean isQuit = false;
          try
          {
             handle finHandle = inHandle;
             Object[] args = cfg.getArguments() == null ? new Object[0] : cfg.getArguments();

             if (cfg.isClass())
             {
                executeScoped(() ->
                {
                   invokeMethodImpl(invocationResult,
                                    isMultiSession,
                                    new character(cfg.getTarget()),
                                    new object(((ObjectResource) finHandle.getResource()).ref()),
                                    cfg.isReturnValue(),
                                    cfg.getModes(),
                                    args);
                   return null;
                });
             }
             else
             {
                isQuit = ((logical) executeScoped(() ->
                {
                   return new logical(invokeImpl(invocationResult,
                                                 isMultiSession,
                                                 exports,
                                                 new character(cfg.getTarget()),
                                                 finHandle,
                                                 cfg.isFunction(),
                                                 cfg.isDynamicFunction(),
                                                 cfg.isSuperCall(),
                                                 cfg.isTransactionDistinct(),
                                                 cfg.getModes(),
                                                 args));
                })).booleanValue();
             }
          }
          finally
          {
             if (cfg.isSingleRun())
             {
                ProcedureManager.deleteInstance(inHandle, cfg.isClass());
             }
             if (resId != null && !inHandle._isValid())
             {
                invocationResult.setInvalidInHandle(true);
                removeProcedureFunc.accept(connectionId, resId);
             }
             else
             {
                invokeDeactivate(deactivateProc, isBoundSupplier.get(), connectionId, setRunningProcFunc);
             }

             if (isQuit && !isNewMultiSession)
             {
                disconnectFunc.accept(connectionId);
             }
          }
       }

       /**
        * Invoke a persistent procedure.
        *
        * @param    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    isMultiSession
        *           Flag to indicate running in a multi-session agent appserver.
        * @param    exports
        *           The export list for remote procedures the client can execute in the current session.
        * @param    activateProc
        *           The session activate procedure.
        * @param    deactivateProc
        *           The session deactivate procedure.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    connectionId
        *           The appserver connection id.
        * @param    agentId
        *           The agent id.
        * @param    threadId
        *           The thread id.
        * @param    sessionId
        *           The appserver session id.
        * @param    cfg
        *           The invoke configuration.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    bindPersistentFunc
        *           The function binding the persistent procedure.
        * @param    unbindPersistentFunc
        *           The function unbinding the persistent procedure.
        * @param    addProcedureFunc
        *           The function adding the persistent procedure.
        * @param    disconnectFunc
        *           The function disconnecting the client.
        */
       public static void runPersistent(AppServerInvocationResult invocationResult,
                                        boolean isMultiSession,
                                        character exports,
                                        String activateProc,
                                        String deactivateProc,
                                        Supplier<Boolean> isBoundSupplier,
                                        String connectionId,
                                        int agentId,
                                        Integer threadId,
                                        Integer sessionId,
                                        InvokeConfig cfg,
                                        BiConsumer<String, String> setRunningProcFunc,
                                        BiConsumer<String, Integer> bindPersistentFunc,
                                        BiConsumer<String, Integer> unbindPersistentFunc,
                                        BiConsumer<String, handle> addProcedureFunc,
                                        Consumer<String> disconnectFunc)
       {
          setOeRequestInfo(cfg, isMultiSession, agentId, threadId, sessionId);

          if (!invokeActivate(activateProc,
                              deactivateProc,
                              isBoundSupplier,
                              connectionId,
                              invocationResult,
                              setRunningProcFunc,
                              disconnectFunc))
          {
             return;
          }

          Object[] args = cfg.getArguments() == null ? new Object[0] : cfg.getArguments();

          invokePersistentImpl(invocationResult,
                               connectionId,
                               agentId,
                               sessionId,
                               new character(cfg.getTarget()),
                               cfg.getProcedureHandle(),
                               cfg.isTransactionDistinct(),
                               exports,
                               setRunningProcFunc,
                               bindPersistentFunc,
                               unbindPersistentFunc,
                               addProcedureFunc,
                               cfg.getModes(),
                               args);

          // called when the procedure file is missing
          invokeDeactivate(deactivateProc, isBoundSupplier.get(), connectionId, setRunningProcFunc);
       }

       /**
        * Invoke the activate procedure on the specified connection ID.
        *
        * @param    activateProc
        *           The session activate procedure.
        * @param    deactivateProc
        *           The session deactivate procedure.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    connectionId
        *           The appserver connection id.
        * @param    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    disconnectFunc
        *           The function disconnecting the client.
        *
        * @return   <code>true</code> if the activate procedure is successfully executed, or if it's not
        *           applicable. <code>false</code> if there was an issue with procedure execution.
        */
       @SuppressWarnings("incomplete-switch")
       public static boolean invokeActivate(String activateProc,
                                            String deactivateProc,
                                            Supplier<Boolean> isBoundSupplier,
                                            String connectionId,
                                            AppServerInvocationResult invocationResult,
                                            BiConsumer<String, String> setRunningProcFunc,
                                            Consumer<String> disconnectFunc)
       {
          if (isBoundSupplier.get() || activateProc == null || activateProc.length() == 0)
          {
             return true;
          }

          setRunningProcFunc.accept(connectionId, activateProc);

          // call the activate procedure (only if the agent is not already bound)
          try
          {
             invokeScoped(new character(activateProc));
          }
          catch (ConditionException ce)
          {
             BlockManager.Condition c = AppserverPackageExports.exceptionToCondition(ce);
             String type = c.toString();
             int code = 0;
             switch (c)
             {
                case ERROR:
                   code = 8025;
                   break;
                case STOP:
                   code = 8026;
                   break;
                case QUIT:
                   code = 8027;
                   break;
             }

             final String msg = "%s activation procedure ended with an %s condition";
             invocationResult.setError(new NumberedException(String.format(msg, activateProc, type), code));
             return false;
          }
          catch (Throwable t)
          {
             boolean ignore = false;

             Throwable cause = t;
             while (cause != null)
             {
                if (cause instanceof UnstoppableExitException)
                {
                   LogicalTerminal.activateBatchMode(true);
                   ignore = true;
                   break;
                }

                cause = cause.getCause();
             }

             if (!ignore)
             {
                invocationResult.setError(t);
                return false;
             }
          }
          finally
          {
             // pass return value
             if (ControlFlowOps.isReturnValueSetInRootScope())
             {
                invocationResult.setResult(ControlFlowOps.getReturnValue());
             }

             // if an error was encountered during activate, call deactivate and disconnect
             if (invocationResult.getError() != null)
             {
                setRunningProcFunc.accept(connectionId, deactivateProc);

                if (!isBoundSupplier.get() && deactivateProc != null && deactivateProc.length() > 0)
                {
                   // call the deactivate procedure (only if the agent is not bound)
                   try
                   {
                      invokeScoped(new character(deactivateProc));
                   }
                   catch (Throwable t)
                   {
                      // ignore all errors
                   }
                }

                disconnectFunc.accept(connectionId);
             }

             setRunningProcFunc.accept(connectionId, null);
          }

          return true;
       }

       /**
        * Invoke the deactivate procedure on the specified connection ID.
        *
        * @param    deactivateProc
        *           The session deactivate procedure.
        * @param    isBound
        *           True if the connection is bound to the appserver session.
        * @param    connectionId
        *           The appserver connection id.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        */
       public static void invokeDeactivate(String deactivateProc,
                                           boolean isBound,
                                           String connectionId,
                                           BiConsumer<String, String> setRunningProcFunc)
       {
          if (!isBound && deactivateProc != null && deactivateProc.length() > 0)
          {
             setRunningProcFunc.accept(connectionId, deactivateProc);

             // call the deactivate procedure (only if the agent is not bound)
             try
             {
                invokeScoped(new character(deactivateProc));
             }
             catch (Throwable t)
             {
                // ignore all errors
             }
             finally
             {
                setRunningProcFunc.accept(connectionId, null);
             }
          }
       }

       /**
        * Invoke an external procedure, internal procedure or user-defined function.
        *
        * @param    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    isMultiSession
        *           Flag to indicate running in a multi-session agent appserver.
        * @param    exports
        *           The export list for remote procedures the client can execute in the current session.
        * @param    name
        *           The legacy 4GL name for the procedure/function.
        * @param    inProc
        *           The external procedure in which to invoke the procedure or function.
        * @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   <core>true</core> if QUIT condition returned, expecting disconnect.
        */
       public static boolean invokeImpl(AppServerInvocationResult invocationResult,
                                        boolean isMultiSession,
                                        character exports,
                                        character name,
                                        handle inProc,
                                        boolean function,
                                        boolean dynamicFunction,
                                        boolean superCall,
                                        boolean transactionDistinct,
                                        String modes,
                                        Object... args)
       {
          boolean isQuit = false;

          // make a copy of the requester's args
          final Object[] requesterArgs = new Object[args.length];
          System.arraycopy(args, 0, requesterArgs, 0, requesterArgs.length);

          try
          {
             preProcessArguments(modes, args);

             Object result = invocationResult.getResult();  // value could be set in appserver activate procedure
             if (inProc == null)
             {
                // force invocation of an external procedure
                ControlFlowOps.invokeExternalProcedure(name, false, null, transactionDistinct,
                                                       exports, modes, args);
                if (ControlFlowOps.isReturnValueSetInRootScope())
                {
                   result = ControlFlowOps.getReturnValue();
                }
             }
             else
             {
                // this is an internal-entry call
                result = ControlFlowOps.invoke(inProc, name, function, dynamicFunction, superCall,
                                               transactionDistinct, modes, args);
                if (!function)
                {
                   result = ControlFlowOps.isReturnValueSetInRootScope() ? ControlFlowOps.getReturnValue() : null;
                }
             }

             if (result instanceof handle)
             {
                // handle results are converted to a container holding only the remote resource
                handle h = (handle) result;
                result = new RemoteResourceImpl(h.isUnknown() ? null : h.toStringMessage());
             }
             invocationResult.setResult(result);
          }
          catch (ErrorConditionException e)
          {
             checkError(invocationResult, e, isMultiSession);
          }
          catch (QuitConditionException e)
          {
             invocationResult.setError(e);
             isQuit = true;
          }
          catch (StopConditionException e)
          {
             if (invokeFailure(e))
             {
                return false;
             }

             throw e;
          }
          catch (Throwable t)
          {
             boolean ignore = false;

             Throwable cause = t;
             while (cause != null)
             {
                if (cause instanceof UnstoppableExitException)
                {
                   LogicalTerminal.activateBatchMode(true);
                   ignore = true;
                   break;
                }

                cause = cause.getCause();
             }

             if (!ignore)
             {
                invocationResult.setError(t);
             }
          }
          finally
          {
             Object[] finalArgs = postProcessArguments(modes, args, requesterArgs);
             invocationResult.setArguments(finalArgs);
             invocationResult.setModes(modes);
          }

          return isQuit;
       }

       /**
        * Invoke a legacy OO method.
        *
        * @param    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    isMultiSession
        *           Flag to indicate running in a multi-session agent appserver.
        * @param    name
        *           The legacy 4GL name for the OO method.
        * @param    ref
        *           The object where to invoke the method.
        * @param    returnValue
        *           Flag indicating if this OO method call must return a value.
        * @param    modes
        *           The method argument's modes, at the caller.
        * @param    args
        *           The arguments.
        */
       private static void invokeMethodImpl(AppServerInvocationResult invocationResult,
                                            boolean isMultiSession,
                                            character name,
                                            object<?> ref,
                                            boolean returnValue,
                                            String modes,
                                            Object... args)
       {
          // make a copy of the requester's args
          final Object[] requesterArgs = new Object[args.length];
          System.arraycopy(args, 0, requesterArgs, 0, requesterArgs.length);

          try
          {
             preProcessArguments(modes, args);

             try
             {
                Object result = null;
                if (returnValue)
                {
                   result = ObjectOps.invoke(ref, name, modes, args);

                   if (result instanceof handle)
                   {
                      // handle results are converted to a container holding only the remote resource
                      handle h = (handle) result;
                      result = new RemoteResourceImpl(h.isUnknown() ? null : h.toStringMessage());
                   }
                   invocationResult.setResult(result);
                }
                else
                {
                   ObjectOps.invokeStandalone(ref, name, modes, args);
                }
             }
             catch (StopConditionException e)
             {
                throw e;
             }
             finally
             {
                Object[] finalArgs = postProcessArguments(modes, args, requesterArgs);
                invocationResult.setArguments(finalArgs);
                invocationResult.setModes(modes);
             }
          }
          catch (ErrorConditionException e)
          {
             checkError(invocationResult, e, isMultiSession);
          }
          catch (QuitConditionException e)
          {
             invocationResult.setError(e);
          }
          catch (Throwable t)
          {
             boolean ignore = false;

             Throwable cause = t;
             while (cause != null)
             {
                if (cause instanceof UnstoppableExitException)
                {
                   LogicalTerminal.activateBatchMode(true);
                   ignore = true;
                   break;
                }

                cause = cause.getCause();
             }

             if (!ignore)
             {
                invocationResult.setError(t);
             }
          }
       }

       /**
        * Invoke a remote external procedure, persistently. The validation of  the program name and
        * arguments will be done on the remote side.
        *
        * @param    invocationResult
        *           The result to be sent back to the remote side.
        * @param    connectionId
        *           The appserver connection id.
        * @param    agentId
        *           The agent id.
        * @param    sessionId
        *           The appserver session id.
        * @param    name
        *           The legacy 4GL name for the procedure.
        * @param    hProc
        *           The handle where to save the persistent procedure.
        * @param    transactionDistinct
        *           Flag indicating if the TRANSACTION DISTINCT clause is in effect.
        * @param    exports
        *           The export list for remote procedures the client can execute in the current session.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    bindPersistentFunc
        *           The function binding the persistent procedure.
        * @param    unbindPersistentFunc
        *           The function unbinding the persistent procedure.
        * @param    addProcedureFunc
        *           The function adding the persistent procedure.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        */
       public static void invokePersistentImpl(AppServerInvocationResult invocationResult,
                                               String connectionId,
                                               Integer agentId,
                                               Integer sessionId,
                                               character name,
                                               handle hProc,
                                               boolean transactionDistinct,
                                               character exports,
                                               BiConsumer<String, String> setRunningProcFunc,
                                               BiConsumer<String, Integer> bindPersistentFunc,
                                               BiConsumer<String, Integer> unbindPersistentFunc,
                                               BiConsumer<String, handle> addProcedureFunc,
                                               String modes,
                                               Object... args)
       {
          executeScoped(() ->
          {
             bindPersistentFunc.accept(connectionId, sessionId);

             setRunningProcFunc.accept(connectionId, name == null ? null : name.toStringMessage());

             // make a copy of the requester's args
             Object[] requesterArgs = new Object[args.length];
             System.arraycopy(args, 0, requesterArgs, 0, requesterArgs.length);

             try
             {
                preProcessArguments(modes, args);

                ControlFlowOps
                   .invokeExternalProcedure(name, true, hProc, transactionDistinct, exports, modes, args);
             }
             catch (Throwable t)
             {
                unbindPersistentFunc.accept(connectionId, sessionId);

                if (t instanceof StopConditionException)
                {
                   if (invokeFailure((StopConditionException) t))
                   {
                      return null;
                   }
                }

                throw t;
             }
             finally
             {
                Object[] finalArgs = postProcessArguments(modes, args, requesterArgs);
                invocationResult.setArguments(finalArgs);
                invocationResult.setModes(modes);
             }

             // set the agent's procedure
             addProcedureFunc.accept(connectionId, hProc);

             if (ControlFlowOps.isReturnValueSetInRootScope())
             {
                invocationResult.setResult(ControlFlowOps.getReturnValue());
             }

             invocationResult.setProcedureId(hProc.toString());
             invocationResult.setAgentId(agentId);

             return null;
          });
       }

       /**
        * 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    invocationResult
        *           The result to be sent back to the remote side.
        * @param    connectionId
        *           The appserver connection id.
        * @param    activateProc
        *           The session activate procedure.
        * @param    deactivateProc
        *           The session deactivate procedure.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    disconnectFunc
        *           The function disconnecting the client.
        * @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.
        */
       public static void invokeWithArgs(AppServerInvocationResult invocationResult,
                                         String connectionId,
                                         String activateProc,
                                         String deactivateProc,
                                         Supplier<Boolean> isBoundSupplier,
                                         BiConsumer<String, String> setRunningProcFunc,
                                         Consumer<String> disconnectFunc,
                                         String procedure,
                                         int[] argTypes,
                                         String modes,
                                         Object[] args)
       {
          if (!invokeActivate(activateProc, deactivateProc, isBoundSupplier, connectionId, invocationResult,
                              setRunningProcFunc, disconnectFunc))
          {
             return;
          }

          setRunningProcFunc.accept(connectionId, procedure);

          // make a copy of the requester's args
          Object[] requesterArgs = new Object[args.length];
          System.arraycopy(args, 0, requesterArgs, 0, requesterArgs.length);

          // add the topmost scope to the TransactionManager
          TransactionManager.pushScope("startup",
                                       TransactionManager.NO_TRANSACTION,
                                       true,
                                       true,
                                       false,
                                       false);
          try
          {
             preProcessArguments(modes, args);

             // initialize the arguments - after they were preprocessed
             AppserverPackageExports.initializeOpenClientArguments(argTypes, modes, args);

             ControlFlowOps.invokeExternalProcedure(new character(procedure), false, null, false, null, null);
          }
          catch (StopConditionException e)
          {
             if (invokeFailure(e))
             {
                return;
             }

             throw e;
          }
          finally
          {
             Object[] finalArgs = postProcessArguments(modes, args, requesterArgs);
             invocationResult.setArguments(finalArgs);
             invocationResult.setModes(modes);

             AppserverPackageExports.clearOpenClientArguments();

             // remove the topmost scope from the TransactionManager
             TransactionManager.popScope();

             invokeDeactivate(deactivateProc,
                              isBoundSupplier.get(),
                              connectionId,
                              setRunningProcFunc);
          }
       }

       /**
        * Create a new instance of the specified qualified class name.
        *
        * @param    invocationResult
        *           The result to be sent back to the remote side.
        * @param    connectionId
        *           The appserver connection id.
        * @param    agentId
        *           The agent id.
        * @param    sessionId
        *           The appserver session id.
        * @param    activateProc
        *           The session activate procedure.
        * @param    deactivateProc
        *           The session deactivate procedure.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    bindPersistentFunc
        *           The function binding the persistent procedure.
        * @param    unbindPersistentFunc
        *           The function unbinding the persistent procedure.
        * @param    addProcedureFunc
        *           The function adding the persistent procedure.
        * @param    disconnectFunc
        *           The function disconnecting the client.
        * @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.
        */
       public static void newInstance(AppServerInvocationResult invocationResult,
                                      String connectionId,
                                      Integer agentId,
                                      Integer sessionId,
                                      String activateProc,
                                      String deactivateProc,
                                      Supplier<Boolean> isBoundSupplier,
                                      BiConsumer<String, String> setRunningProcFunc,
                                      BiConsumer<String, Integer> bindPersistentFunc,
                                      BiConsumer<String, Integer> unbindPersistentFunc,
                                      BiConsumer<String, handle> addProcedureFunc,
                                      Consumer<String> disconnectFunc,
                                      character name,
                                      String modes,
                                      Object[] args)
       {
          if (!invokeActivate(activateProc, deactivateProc, isBoundSupplier, connectionId, invocationResult,
                              setRunningProcFunc, disconnectFunc))
          {
             return;
          }

          setRunningProcFunc.accept(connectionId, name == null ? null : name.toStringMessage());

          try
          {
             final handle hProc = new handle();

             // add the topmost scope to the TransactionManager
             TransactionManager.pushScope("startup",
                                          TransactionManager.NO_TRANSACTION,
                                          true,
                                          true,
                                          false,
                                          false);
             object<? extends BaseObject> instance = new ObjectVar<>(BaseObject.class);

             // make a copy of the requester's args
             Object[] requesterArgs = new Object[args.length];
             System.arraycopy(args, 0, requesterArgs, 0, requesterArgs.length);

             try
             {
                bindPersistentFunc.accept(connectionId, sessionId);

                preProcessArguments(modes, args);

                // this will 'pin' at least a reference, so that it will not be implicitly deleted
                instance.assign(ObjectOps.newDynamicInstance(name, modes, args));

                if (!instance.isUnknown())
                {
                   hProc.assign(new ExternalProgramWrapper(instance.ref()));
                }
             }
             catch (Throwable t)
             {
                unbindPersistentFunc.accept(connectionId, sessionId);

                if (t instanceof StopConditionException)
                {
                   if (invokeFailure((StopConditionException) t))
                   {
                      return;
                   }
                }

                throw t;
             }
             finally
             {
                Object[] finalArgs = postProcessArguments(modes, args, requesterArgs);
                invocationResult.setArguments(finalArgs);
                invocationResult.setModes(modes);

                // remove the topmost scope from the TransactionManager
                TransactionManager.popScope();
             }

             // set the agent's procedure
             addProcedureFunc.accept(connectionId, hProc);

             invocationResult.setProcedureId(hProc.toString());
             invocationResult.setAgentId(agentId);
          }
          finally
          {
             invokeDeactivate(deactivateProc, isBoundSupplier.get(), connectionId, setRunningProcFunc);
          }
       }

       /**
        * Invoke a method in a remotely instantiated class.
        *
        * @param    invocationResult
        *           The result to be sent back to the remote side.
        * @param    isMultiSession
        *           Flag to indicate running in a multi-session agent appserver.
        * @param    connectionId
        *           The appserver connection id.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @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    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The method's arguments.
        */
       public static void invokeMethod(AppServerInvocationResult invocationResult,
                                       boolean isMultiSession,
                                       String connectionId,
                                       Supplier<Boolean> isBoundSupplier,
                                       BiConsumer<String, String> setRunningProcFunc,
                                       boolean returnValue,
                                       character name,
                                       String resId,
                                       String modes,
                                       Object[] args)
       {
          if (!isBoundSupplier.get())
          {
             // TODO: error out?
             return;
          }

          // resolve the procedure - this is actually 'extProg', validated when the agent was acquired.
          handle proc = handle.fromString(resId);

          String className = proc.get().getClass().getName();
          String methodName = name.toStringMessage();
          setRunningProcFunc.accept(connectionId, className + " # " + methodName);

          object<? extends BaseObject> ref = new object<>((BaseObject) proc.get());

          // add the topmost scope to the TransactionManager
          TransactionManager.pushScope("startup",
                                       TransactionManager.NO_TRANSACTION,
                                       true,
                                       true,
                                       false,
                                       false);

          try
          {
             invokeMethodImpl(invocationResult,
                              isMultiSession,
                              name,
                              ref,
                              returnValue,
                              modes,
                              args);
          }
          finally
          {
             // remove the topmost scope from the TransactionManager
             TransactionManager.popScope();
          }
       }

       /**
        * 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    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    connectionId
        *           The appserver connection id.
        * @param    isMultiSession
        *           Flag to indicate running in a multi-session agent appserver.
        * @param    activateProc
        *           The session activate procedure.
        * @param    deactivateProc
        *           The session deactivate procedure.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    disconnectFunc
        *           The function disconnecting the client.
        * @param    cfg
        *           The configuration with the Java method to be invoked.
        */
       public static void invokeJavaMethod(AppServerInvocationResult invocationResult,
                                           String connectionId,
                                           boolean isMultiSession,
                                           String activateProc,
                                           String deactivateProc,
                                           Supplier<Boolean> isBoundSupplier,
                                           BiConsumer<String, String> setRunningProcFunc,
                                           Consumer<String> disconnectFunc,
                                           JavaInvokeConfig cfg)
       {
          if (!invokeActivate(activateProc, deactivateProc, isBoundSupplier, connectionId, invocationResult,
                              setRunningProcFunc, disconnectFunc))
          {
             return;
          }

          setRunningProcFunc.accept(connectionId, cfg.getClazz().getName() + " # " + cfg.getTarget());

          boolean disconnect = false;
          try
          {
             disconnect = ((logical) executeScoped(() -> {
                boolean isQuit = false;

                Object[] args = cfg.getArguments() == null ? new Object[0] : cfg.getArguments();

                try
                {
                   Method mthd = cfg.getMethod();
                   Class<?> clz = cfg.getClazz();
                   Object result = null;
                   Object instance = null;
                   if (!Modifier.isStatic(mthd.getModifiers()))
                   {
                      instance = clz.getDeclaredConstructor().newInstance();
                   }
                   result = mthd.invoke(instance, args);

                   invocationResult.setResult(result);
                }
                catch (ErrorConditionException e)
                {
                   checkError(invocationResult, e, isMultiSession);
                }
                catch (QuitConditionException e)
                {
                   invocationResult.setError(e);
                   isQuit = true;
                }
                catch (StopConditionException e)
                {
                   if (invokeFailure(e))
                   {
                      return new logical(false);
                   }

                   throw e;
                }
                catch (Throwable t)
                {
                   boolean ignore = false;

                   Throwable cause = t;
                   while (cause != null)
                   {
                      if (cause instanceof UnstoppableExitException)
                      {
                         LogicalTerminal.activateBatchMode(true);
                         ignore = true;
                         break;
                      }

                      cause = cause.getCause();
                   }

                   if (!ignore)
                   {
                      invocationResult.setError(t);
                   }
                }
                finally
                {
                   invocationResult.setArguments(args);
                }

                return new logical(isQuit);
             }))
                .booleanValue();
          }
          finally
          {
             invokeDeactivate(deactivateProc, isBoundSupplier.get(), connectionId, setRunningProcFunc);

             if (disconnect)
             {
                disconnectFunc.accept(connectionId);
             }
          }
       }

       /**
        * Obtain a proxy for the given external procedure. This will also reserve an agent session which will
        * be used for all subsequent invocation (including external procedure initialization).
        *
        * @param    hProc
        *           The handle to be assigned the proxy procedure.
        * @param    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    exports
        *           The export list for remote procedures the client can execute in the current session.
        * @param    activateProc
        *           The session activate procedure.
        * @param    deactivateProc
        *           The session deactivate procedure.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    connectionId
        *           The appserver connection id.
        * @param    agentId
        *           The agent id.
        * @param    sessionId
        *           The appserver session id.
        * @param    externalProcName
        *           A name of a remote external procedure.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    bindPersistentFunc
        *           The function binding the persistent procedure.
        * @param    addProcedureFunc
        *           The function adding the persistent procedure.
        * @param    disconnectFunc
        *           The function disconnecting the client.
        */
       public static void obtainProxy(handle hProc,
                                      AppServerInvocationResult invocationResult,
                                      character exports,
                                      String activateProc,
                                      String deactivateProc,
                                      Supplier<Boolean> isBoundSupplier,
                                      String connectionId,
                                      Integer agentId,
                                      Integer sessionId,
                                      String externalProcName,
                                      BiConsumer<String, String> setRunningProcFunc,
                                      BiConsumer<String, Integer> bindPersistentFunc,
                                      BiConsumer<String, handle> addProcedureFunc,
                                      Consumer<String> disconnectFunc)
       {
          if (!invokeActivate(activateProc, deactivateProc, isBoundSupplier, connectionId, invocationResult,
                              setRunningProcFunc, disconnectFunc))
          {
             return;
          }

          setRunningProcFunc.accept(connectionId, externalProcName);

          // add the topmost scope to the TransactionManager; this will be popped only when:
          // a. the ext prog can't be resolved
          // b. the ext prog has been initialized
          TransactionManager.pushScope("startup",
                                       TransactionManager.NO_TRANSACTION,
                                       true,
                                       true,
                                       false,
                                       false);

          ExternalProgramWrapper extProg = null;

          try
          {
             extProg = ControlFlowOps.resolveExternalProcedure(externalProcName, exports);

             if (extProg != null)
             {
                hProc.assign(extProg);

                // make it bound only if the invocation was successful
                bindPersistentFunc.accept(connectionId, sessionId);

                // associate the agent with this ext prog
                addProcedureFunc.accept(connectionId, hProc);
             }
          }
          finally
          {
             if (extProg == null)
             {
                // remove the topmost scope from the TransactionManager, as the ext prog could
                // not be resolved
                TransactionManager.popScope();

                invokeDeactivate(deactivateProc, isBoundSupplier.get(), connectionId, setRunningProcFunc);
             }
             else
             {
                invocationResult.setAgentId(agentId);
             }
          }
       }

       /**
        * Initialize the specified proxy procedure, by calling the <code>execute</code> method.
        *
        * @param    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    persistentProcResId
        *           The external procedure resource id.
        * @param    extProg
        *           The external procedure associated with the appserver / connection.
        * @param    connectionId
        *           The appserver connection id.
        * @param    agentId
        *           The agent id.
        * @param    sessionId
        *           The appserver session id.
        * @param    requestId
        *           The ID of the request.
        * @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.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        * @param    removeProcedureFunc
        *           The function removing the procedure.
        * @param    unbindPersistentFunc
        *           The function unbinding the persistent procedure.
        * @param    isBoundSupplier
        *           The supplier returning the bound state of the session.
        * @param    deactivateProc
        *           The session deactivate procedure.
        */
       public static void initializeProxy(AppServerInvocationResult invocationResult,
                                          String persistentProcResId,
                                          ExternalProgramWrapper extProg,
                                          String connectionId,
                                          Integer agentId,
                                          Integer sessionId,
                                          int requestId,
                                          boolean transactionDistinct,
                                          String modes,
                                          Object[] args,
                                          BiConsumer<String, String> setRunningProcFunc,
                                          BiConsumer<String, String> removeProcedureFunc,
                                          BiConsumer<String, Integer> unbindPersistentFunc,
                                          Supplier<Boolean> isBoundSupplier,
                                          String deactivateProc)
       {
          setRunningProcFunc.accept(connectionId, extProg.getFileName().toStringMessage());

          // make a copy of the requester's args
          Object[] requesterArgs = new Object[args.length];
          System.arraycopy(args, 0, requesterArgs, 0, requesterArgs.length);

          try
          {
             // WARNING: do not add a scope here - obtainProxy will add it and keep it on the stack

             try
             {
                preProcessArguments(modes, args);

                ControlFlowOps.initializeExternalProcedure(extProg,
                                                           transactionDistinct,
                                                           modes,
                                                           args);
             }
             catch (StopConditionException e)
             {
                if (invokeFailure(e))
                {
                   return;
                }

                throw e;
             }
             finally
             {
                Object[] finalArgs = postProcessArguments(modes, args, requesterArgs);
                invocationResult.setArguments(finalArgs);
                invocationResult.setModes(modes);

                // remove the topmost scope from the TransactionManager.  this scope was added by obtainProxy
                TransactionManager.popScope();
             }

             if (ControlFlowOps.isReturnValueSetInRootScope())
             {
                invocationResult.setResult(ControlFlowOps.getReturnValue());
             }
             invocationResult.setProcedureId(new handle(extProg).toString());
             invocationResult.setAgentId(agentId);
             invocationResult.setSessionId(sessionId);
             invocationResult.setRequestId(String.valueOf(requestId));
          }
          finally
          {
             if (!extProg.valid())
             {
                removeProcedureFunc.accept(connectionId, persistentProcResId);
                unbindPersistentFunc.accept(connectionId, sessionId);
             }
             invokeDeactivate(deactivateProc, isBoundSupplier.get(), connectionId, setRunningProcFunc);
          }
       }

       /**
        * 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    invocationResult
        *           The result where to save the arguments and any return value.
        * @param    connectionId
        *           The appserver connection id.
        * @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    target
        *           The request original target.
        * @param    request
        *           The Jetty request.
        * @param    response
        *           The Jetty response.
        * @param    setRunningProcFunc
        *           The function setting the running procedure.
        */
       public static void handleWebService(AppServerInvocationResult invocationResult,
                                           String connectionId,
                                           Class<? extends WebHandler> handler,
                                           String basepath,
                                           String[] paths,
                                           String target,
                                           HttpServletRequest request,
                                           HttpServletResponse response,
                                           BiConsumer<String, String> setRunningProcFunc)
       {
          object<WebHandler> ohandler = TypeFactory.object(WebHandler.class);

          setRunningProcFunc.accept(connectionId, target);

          // add the topmost scope to the TransactionManager
          TransactionManager.pushScope("startup",
                                       TransactionManager.NO_TRANSACTION,
                                       true,
                                       true,
                                       false,
                                       false);

          try
          {
             try
             {
                WebHandler.initialize(basepath, target, paths, request, response);

                ohandler.assign(ObjectOps.newInstance(handler));

                if (!ohandler._isValid())
                {
                   // TODO:
                   return;
                }
                @SuppressWarnings("unused")
                integer res = ohandler.ref().handleRequest();
                // TODO: non-zero means a static error page must be shown, for that error...
             }
             catch (Throwable t)
             {
                invocationResult.setError(t);
             }
             finally
             {
                if (ohandler._isValid())
                {
                   ObjectOps.delete(ohandler);
                }
                WebHandler.clear();
             }
          }
          finally
          {
             // remove the topmost scope from the TransactionManager
             TransactionManager.popScope();
          }
       }

       /**
        * Check if the specified ERROR condition needs to be reported.
        *
        * @param    invocationResult
        *           The result where to save the error.
        * @param    e
        *           The ERROR condition.
        * @param    isMultiSession
        *           Flag to indicate running in a multi-session agent appserver.
        */
       public static void checkError(AppServerInvocationResult invocationResult,
                                     ErrorConditionException e,
                                     boolean isMultiSession)
       {
          if (ControlFlowOps.isReturnValueSetInRootScope())
          {
             if (isMultiSession)
             {
                invocationResult.setError(e);
             }
             else
             {
                String msg = ControlFlowOps.getReturnValue().toStringMessage();
                invocationResult.setError(new ErrorConditionException(msg));
             }
             return;
          }

          Throwable cause = e.getCause();
          if (cause instanceof NumberedException)
          {
             // only the following codes will be sent to the remote side
             // TODO: this needs to be done only if the errors are caused by problems in
             // resolving the appserver call target, and not by nested RUN statements
             final Set<Integer> notifyErrs = new HashSet<Integer>();
             final Integer[] codes =
                { 6349, 3230, 1005, 3234, 5729, 2570, 11428, 12290, 15472, 5639, 6445, 8030,
                   12324 };
             notifyErrs.addAll(Arrays.asList(codes));

             NumberedException ne = (NumberedException) cause;
             int num = ne.getNumber();
             if (notifyErrs.contains(num))
             {
                invocationResult.setError(e);
             }
          }

          if (invocationResult.getError() == null)
          {
             cause = e;
             while (cause != null)
             {
                if (cause instanceof ConditionException)
                {
                   invocationResult.setError(cause);
                   break;
                }
                cause = cause.getCause();
             }
          }

          if (invocationResult.getError() == null)
          {
             invocationResult.setError(e);
          }
       }

       /**
        * Execute the specified task scoped to a topmost, global block.
        *
        * @param    task
        *           The task to execute.
        *
        * @return   The value as returned by the task.
        */
       public static BaseDataType executeScoped(Supplier<BaseDataType> task)
       {
          // add the topmost scope to the TransactionManager
          TransactionManager.pushScope("startup",
                                       TransactionManager.NO_TRANSACTION,
                                       true,
                                       true,
                                       false,
                                       false);
          try
          {
             return task.get();
          }
          catch (StopConditionException e)
          {
             // check if this can be converted to error
             if (invokeFailure(e))
             {
                return new unknown();
             }

             throw e;
          }
          finally
          {
             // remove the topmost scope from the TransactionManager
             TransactionManager.popScope();
          }
       }

       /**
        * Populates the CURRENT-REQUEST-INFO & CURRENT-RESPONSE-INFO.
        *
        * @param    cfg
        *           The invoke configuration.
        * @param    isMultiSession
        *           Flag to indicate running in a multi-session agent appserver.
        * @param    agentId
        *           The agent id.
        * @param    threadId
        *           The thread id.
        * @param    sessionId
        *           The appserver session id.
        */
       static void setOeRequestInfo(InvokeConfig cfg,
                                    boolean isMultiSession,
                                    Integer agentId,
                                    Integer threadId,
                                    Integer sessionId)
       {
          object<? extends OerequestInfo> oreq =
             (object) executeScoped(() -> SessionUtils.currentRequestInfo());
          object<? extends OerequestInfo> oresp =
             (object) executeScoped(() -> SessionUtils.currentResponseInfo());

          OerequestInfo req = AppserverPackageExports.convertToOeRequest(oreq);
          if (req == null)
          {
             return;
          }

          OerequestInfo resp = AppserverPackageExports.convertToOeRequest(oresp);
          executeScoped(() ->
                        {
                           if (cfg != null)
                           {
                              cfg.restoreRequest(req, isMultiSession ? agentId : null, sessionId, threadId);
                           }
                           else
                           {
                              req.initialize(null, null, null, null, null, agentId, sessionId, threadId, null);
                           }
                           resp.initialize(req, false);
                           return null;
                        });
       }

       /**
        * Check if the given STOP condition is from an invoke failure.
        *
        * @param    e
        *           The STOP condition.
        *
        * @return   <code>true</code> if the cause if this STOP is a {@link NumberedException} known
        *           to be produced by invoke failures.
        */
       private static boolean invokeFailure(StopConditionException e)
       {
          Throwable cause = e.getCause();
          if (cause != null && cause instanceof NumberedException)
          {
             // this specific case treats the error raised by ControlFlowOps.invokeFailure, which will
             // always set the topmost cause as a NumberedException
             NumberedException ne = (NumberedException) cause;
             if (ne.getNumber() == 293)
             {
                // throws a ControlFlowOps.invokeFailure incoming error to allow the current command
                // to abort (which is a RUN of an external program) and will just 'eat' the exception
                // and copy it to the RemoteInvocationResult instance; it should be safe to directly
                // instantiate the error because we are outside of 4GL normal behavior; anyway, the
                // idea is also 'this needs to be thrown'

                // raise ERROR condition if the external procedure was not found
                if (e instanceof StopConditionException)
                {
                   throw e;
                }
                else
                {
                   throw new ErrorConditionException(e);
                }
             }
          }

          return false;
       }

       /**
        * Preprocess the arguments sent by the requester so that they are converted to instances
        * compatible with the P2J runtime.  This includes:
        * <ul>
        * <li>Creating the handles for all received resources</li>
        * <li>Creating the temp-tables for the received {@link TableWrapper} instances</li>
        * <li>Creating {@link memptr} instances from all {@link MemoryBuffer} instances</li>
        * <li>Creating {@link object} instances from all {@link LegacyObject} instances</li>
        * </ul>
        *
        * @param    requesterModes
        *           The parameter modes sent by the requester.
        * @param    args
        *           The argument list sent by the requester.  The elements in this array will be
        *           changed as needed.
        */
       private static void preProcessArguments(String requesterModes, Object[] args)
       {
          ControlFlowOps.resetReturnValueSetInRootScope();

          String modes = requesterModes;
          if (modes == null || modes.length() != args.length)
          {
             // no modes specified or incompatible modes
             return;
          }

          // a bogus class... for dataset parameters
          ProcedureManager.setInstantingExternalProgramClass(Object.class);
          for (int i = 0; i < modes.length(); i++)
          {
             Object arg = args[i];

             boolean inputOutput = modes.charAt(i) == 'U';
             boolean output = modes.charAt(i) == 'O';
             boolean input = modes.charAt(i) == 'I';

             if (arg instanceof TableWrapper)
             {
                // a result set will be sent only if the requester and the appserver are not in the
                // same JVM
                TableWrapper rs = (TableWrapper) arg;
                arg = new TableParameter(rs,
                                         ParameterOption.APPEND,
                                         input || inputOutput,
                                         output || inputOutput);
                ((TableParameter) arg).setParameterIndex(i + 1);
             }
             else if (arg instanceof DatasetWrapper)
             {
                DatasetWrapper ds = (DatasetWrapper) arg;
                arg = new DataSetParameter(ds);
                ((DataSetParameter) arg).setParameterIndex(i + 1);
             }
             else
             {
                boolean isArray = arg.getClass().isArray();
                Object[] data = null;

                if (isArray)
                {
                   // check if this argument is an array
                   Class<?> ctype = arg.getClass().getComponentType();
                   if (ctype == MemoryBuffer.class)
                   {
                      ctype = memptr.class;
                   }
                   else if (ctype == LegacyObject.class)
                   {
                      ctype = ObjectVar.class;
                   }

                   data = (Object[]) Array.newInstance(ctype, Array.getLength(arg));

                   for (int j = 0; j < data.length; j++)
                   {
                      Object jarg = Array.get(arg, j);
                      if (jarg instanceof MemoryBuffer)
                      {
                         jarg = new memptr(((MemoryBuffer) jarg).getValue());
                      }
                      else if (jarg instanceof LegacyObject)
                      {
                         LegacyObject ser = (LegacyObject) jarg;
                         jarg = new ObjectVar(ser.getType());
                         _BaseObject_ ref = ser.restore();
                         ((ObjectVar) jarg).assign(ref);
                      }
                      data[j] = jarg;
                   }
                }
                else
                {
                   data = new Object[] { arg };
                }

                for (int j = 0; j < data.length; j++)
                {
                   Object darg = data[j];

                   if (darg instanceof RemoteResource)
                   {
                      String resid = ((RemoteResource) darg).getResourceId();
                      darg = (resid == null ? new handle() : handle.fromString(resid));
                   }
                   else if (darg instanceof MemoryBuffer)
                   {
                      // convert MemoryBuffer instances to memptr instances
                      byte[] bytes = ((MemoryBuffer) darg).getValue();
                      darg = bytes == null ? new memptr() : new memptr(bytes);
                   }
                   else if (darg instanceof LegacyObject)
                   {
                      LegacyObject ser = (LegacyObject) darg;
                      _BaseObject_ ref = ser.restore();
                      ObjectVar o = new ObjectVar(ser.getType());
                      o.assign(ref);
                      darg = o;
                   }

                   data[j] = darg;
                }

                if (isArray)
                {
                   if (inputOutput || output)
                   {
                      BaseDataType[] extArg = (BaseDataType[]) data;
                      int idx = i;

                      if (inputOutput)
                      {
                         arg = new InputOutputExtentParameter<BaseDataType>()
                         {
                            @Override
                            public BaseDataType[] getVariable()
                            {
                               return extArg;
                            }

                            @Override
                            public void setVariable(BaseDataType[] newRef)
                            {
                               args[idx] = newRef;
                            }
                         };
                      }
                      else
                      {
                         arg = new OutputExtentParameter<BaseDataType>()
                         {
                            @Override
                            public BaseDataType[] getVariable()
                            {
                               return extArg;
                            }

                            @Override
                            public void setVariable(BaseDataType[] newRef)
                            {
                               args[idx] = newRef;
                            }
                         };
                      }
                   }
                   else
                   {
                      arg = data;
                   }
                }
                else
                {
                   arg = data[0];
                }
             }

             args[i] = arg;
          }
          ProcedureManager.setInstantingExternalProgramClass(null);
       }

       /**
        * Post-process the given arguments so that all OUTPUT and INPUT-OUTPUT arguments will be sent
        * back to the requester. This includes:
        * <ul>
        *    <li>Serializing all table parameters via {@link TableWrapper} instances</li>
        *    <li>Sending the resource IDs for all handle parameters</li>
        *    <li>Converting all {@link memptr} instances to {@link MemoryBuffer} instances</li>
        *    <li>Converting all {@link object} instances to {@link LegacyObject} instances</li>
        * </ul>
        *
        * @param    requesterModes
        *           The parameter modes sent by the requester.
        * @param    localArgs
        *           The argument list after the call has been performed.
        * @param    requesterArgs
        *           The argument list sent by the requester.
        *
        * @return   The postprocessed arguments, to be sent back to the requester.
        */
       private static Object[] postProcessArguments(String requesterModes,
                                                    Object[] localArgs,
                                                    Object[] requesterArgs)
       {
          try
          {
             String modes = requesterModes;
             if (modes == null || requesterArgs.length != modes.length())
             {
                // no modes specified, so no arguments can be returned - assume all are input
                return new Object[0];
             }

             Object[] arguments = new Object[localArgs.length];
             for (int i = 0; i < modes.length(); i++)
             {
                char mode = modes.charAt(i);

                // in input mode, the argument is not sent back
                Object localArg = (mode == 'O' || mode == 'U' ? localArgs[i] : null);

                if (localArg == null)
                {
                   // nothing to post-process if the local arg was determined to be null
                   arguments[i] = null;
                   continue;
                }
                Object requesterArg = requesterArgs[i];

                // if output/input-output mode, send back the data.

                // a table parameter is serialized to a result set only if the requester has sent a
                // result set
                if (localArg instanceof TableParameter && requesterArg instanceof TableWrapper)
                {
                   // we have a DMO on the remote side, use the P2J persistence layer to get the data
                   TableParameter tableParam = (TableParameter) localArg;
                   localArg = tableParam.getResultSet();
                }
                else if (localArg instanceof DataSetParameter && requesterArg instanceof DatasetWrapper)
                {
                   DataSetParameter dsParam = (DataSetParameter) localArg;
                   localArg = dsParam.getDatasetWrapper();
                }
                else
                {
                   if (localArg instanceof AbstractExtentParameter)
                   {
                      localArg = ((AbstractExtentParameter) localArg).getVariableSafe();
                   }
                   boolean isArray = localArg.getClass().isArray();
                   Object[] data = null;

                   if (isArray)
                   {
                      // check if this argument is an array
                      Class<?> ctype = localArg.getClass().getComponentType();
                      if (ctype == memptr.class)
                      {
                         ctype = MemoryBuffer.class;
                      }
                      else if (object.class.isAssignableFrom(ctype))
                      {
                         ctype = LegacyObject.class;
                      }

                      data = (Object[]) Array.newInstance(ctype, Array.getLength(localArg));

                      for (int j = 0; j < data.length; j++)
                      {
                         Object jarg = Array.get(localArg, j);
                         if (jarg.getClass() == memptr.class)
                         {
                            jarg = new MemoryBuffer(((memptr) jarg).asByteArray());
                         }
                         else if (object.class.isAssignableFrom(jarg.getClass()))
                         {
                            jarg = new LegacyObject((object) jarg);
                         }

                         data[j] = jarg;
                      }
                   }
                   else
                   {
                      data = new Object[] { localArg };
                   }

                   for (int j = 0; j < data.length; j++)
                   {
                      Object dlocalArg = data[j];

                      if (dlocalArg instanceof handle)
                      {
                         handle h = (handle) dlocalArg;

                         dlocalArg = new RemoteResourceImpl(h.isUnknown() ? null : h.toStringMessage());
                      }
                      else if (dlocalArg instanceof memptr)
                      {
                         // convert memptr instances to MemoryBuffer instances
                         dlocalArg = new MemoryBuffer(((memptr) dlocalArg).asByteArray());
                      }
                      else if (dlocalArg instanceof object)
                      {
                         // convert memptr instances to MemoryBuffer instances
                         dlocalArg = AppserverPackageExports.convertToLegacyObject(dlocalArg);
                      }

                      data[j] = dlocalArg;
                   }

                   localArg = (isArray ? data : data[0]);
                }

                arguments[i] = localArg;
             }

             return arguments;
          }
          finally
          {
             for (int i = 0; i < localArgs.length; i++)
             {
                Object arg = localArgs[i];

                if (arg instanceof memptr)
                {
                   // release the memory for all memptr arguments
                   ((memptr) arg).setLength(0);
                }
             }
          }
       }

       /**
        * Invoke an external procedure with no parameters.
        *
        * @param    name
        *           The legacy 4GL name for the procedure/function.
        */
       private static void invokeScoped(character name)
       {
          executeScoped(() ->
                        {
                           // force invocation of an external procedure
                           ControlFlowOps.invokeExternalProcedure(name, false, null, false, null, null);
                           return ControlFlowOps.getReturnValue();
                        });
       }
    }