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