# LegacyJavaAppserverClient.java

    /*
    ** Module   : LegacyJavaAppserverClient.java
    ** Abstract : Base class for Java clients connecting to a FWD server running in the same JVM.
    **
    ** Copyright (c) 2020-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20201008 Created initial version.
    **     CA  20210917 Javadoc fixes.
    **     CA  20211214 Refactored to use a worker pool, instead of switching context and executing the remote
    **                  request directly from the web thread.  Now it uses the same infrastructure as the legacy
    **                  SOAP and REST support.
    **     CA  20211216 Allow the clients to know if the connection pool has initialized, so they can treat this
    **                  as a 404/NOT-FOUND.
    **     CA  20210104 Added support for denormalized extent fields, for .NET client compatibility.
    **     CA  20220114 Changed sessionId to long.
    **     CA  20220208 Any exception thrown by the remote appserver call is wrapped in a RuntimeAppserverError.
    **     CA  20220428 Added function, SINGLET, SINGLE-RUN and non-persistent invoke modes.
    **     CA  20220513 Added 'cancelAllRequests'.
    **     CA  20221031 Refactored to create a RemoteLegacyJavaAppserverClient for remote connections.
    **     CA  20221116 Added 'unknownValuesForFields', a flag which when set, will force the DataObject.get to
    **                  return null even for Java native types - without this flag, DataObject.get will return
    **                  i.e. 0 or false for int/boolean native types.
    ** 002 CA  20230221 Refactored to always use InvokeConfig when performing remote calls.
    ** 003 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 004 CA  20230712 Added 'invokeWithArgs', an API which can send any number of arguments to a remote program
    **                  acting as a controller, which can prepare and execute the real target.
    **                  Backed out CA/20230221, as InvokeConfig APIs can act with SINGLETON/SINGLE-RUN.
    **                  Added 'setParameterOrig', useful for mocking the FWD server via sub-classing
    **                  LegacyJavaAppserverClient.
    ** 005 GBB 20240610 LegacyAppServerWork replaced by Consumer<LegacyServiceWorker>.
    ** 006 LS  20240819 Added 'mergeTables', a flag which when set, will merge the response instead of replacing
    **                  it in the dataset
    ** 007 CA  20250324 Improvements for remote FWD OpenClient connections: ensure that the remote side uses a
    **                  single connection to the FWD server, and also use a client-side pool of worker threads to
    **                  send the requests (the pool size is in sync with the maximum agents available to process
    **                  requests).
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import java.util.*;

    import javax.servlet.http.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.p2j.web.*;

    /**
     * Provides helper APIs to interact with the Appserver running in the same FWD server.
     */
    public class LegacyJavaAppserverClient
    extends LegacyJavaAppserver
    {
       /** Anonymous log instance. */
       protected static final CentralLogger LOG = CentralLogger.get(LegacyJavaAppserverClient.class);

       /** The pool of workers. */
       protected final AppServerConnectionPool pool;

       /** The session ID associated with this client. */
       protected final long sessionID;

       /** The appserver connection ID for this client.  May be shared by multiple client sessions. */
       protected String connectionID = null;

       /**
        * Initialize this client with the given worker pool.
        *
        * @param    pool
        *           The pool of workers where tasks will be forwarded, to be executed on the remote appserver.
        */
       public LegacyJavaAppserverClient(AppServerConnectionPool pool)
       {
          this(pool, true);
       }

       /**
        * Initialize this client with the given worker pool.
        *
        * @param    pool
        *           The pool of workers where tasks will be forwarded, to be executed on the remote appserver.
        * @param    normalizedExtent
        *           The extent field mode.
        */
       public LegacyJavaAppserverClient(AppServerConnectionPool pool, boolean normalizedExtent)
       {
          super(normalizedExtent);

          this.pool = pool;
          this.sessionID = pool.nextSessionId();
       }

       /**
        * Establish a remote client connection, and connect it to the target appserver.
        * <p>
        * This connection established via instances of this class is not thread-safe - the client can either pool
        * the connections and check them out as needed, or otherwise ensure that the connection is used on only one
        * thread (this includes the {@link #connect}, invocations and {@link #disconnect} calls.
        * <p>
        * All posted tasks are executed with the defined <code>timeout</code>.  By default, this is zero, waiting
        * for the task to complete.  Otherwise, the task will be interrupted at the timeout expiration.
        *
        * @param    cfg
        *           The configuration to connect to the remote FWD server.
        * @param    appserver
        *           The appserver name for the target connection.
        * @param    timeout
        *           The timeout of the request.
        *
        * @return   The client, used to perform calls on that connection.
        */
       public static LegacyJavaAppserverClient connectRemote(BootstrapConfig cfg, String appserver, int timeout)
       {
          return connectRemote(cfg, appserver, true, timeout);
       }

       /**
        * Establish a remote client connection, and connect it to the target appserver.
        * <p>
        * This connection established via instances of this class is not thread-safe - the client can either pool
        * the connections and check them out as needed, or otherwise ensure that the connection is used on only one
        * thread (this includes the {@link #connect}, invocations and {@link #disconnect} calls.
        * <p>
        * All posted tasks are executed with the defined <code>timeout</code>.  By default, this is zero, waiting
        * for the task to complete.  Otherwise, the task will be interrupted at the timeout expiration.
        *
        * @param    cfg
        *           The configuration to connect to the remote FWD server.
        * @param    appserver
        *           The appserver name for the target connection.
        *
        * @return   The client, used to perform calls on that connection.
        */
       public static LegacyJavaAppserverClient connectRemote(BootstrapConfig cfg, String appserver)
       {
          return connectRemote(cfg, appserver, true, 0);
       }

       /**
        * Establish a remote client connection, and connect it to the target appserver.
        * <p>
        * This connection established via instances of this class is not thread-safe - the client can either pool
        * the connections and check them out as needed, or otherwise ensure that the connection is used on only one
        * thread (this includes the {@link #connect}, invocations and {@link #disconnect} calls.
        * <p>
        * All posted tasks are executed with the defined <code>timeout</code>.  By default, this is zero, waiting
        * for the task to complete.  Otherwise, the task will be interrupted at the timeout expiration.
        *
        * @param    cfg
        *           The configuration to connect to the remote FWD server.
        * @param    appserver
        *           The appserver name for the target connection.
        * @param    sessionFree
        *           Flag indicating if session-free mode is used.
        *
        * @return   The client, used to perform calls on that connection.
        */
       public static LegacyJavaAppserverClient connectRemote(BootstrapConfig cfg,
                                                             String          appserver,
                                                             boolean         sessionFree)
       {
          return connectRemote(cfg, appserver, sessionFree, 0);
       }

       /**
        * Establish a remote client connection, and connect it to the target appserver.
        * <p>
        * This connection established via instances of this class is not thread-safe - the client can either pool
        * the connections and check them out as needed, or otherwise ensure that the connection is used on only one
        * thread (this includes the {@link #connect}, invocations and {@link #disconnect} calls.
        * <p>
        * All posted tasks are executed with the defined <code>timeout</code>.  By default, this is zero, waiting
        * for the task to complete.  Otherwise, the task will be interrupted at the timeout expiration.
        *
        * @param    cfg
        *           The configuration to connect to the remote FWD server.
        * @param    appserver
        *           The appserver name for the target connection.
        * @param    sessionFree
        *           Flag indicating if session-free mode is used.
        * @param    timeout
        *           The timeout of the request.
        *
        * @return   The client, used to perform calls on that connection.
        */
       public static LegacyJavaAppserverClient connectRemote(BootstrapConfig cfg,
                                                             String          appserver,
                                                             boolean         sessionFree,
                                                             int             timeout)
       {
          RemoteAppServerConnectionPool pool = new RemoteAppServerConnectionPool(cfg, appserver, sessionFree);
          pool.setTimeout(timeout);
          pool.initialize();

          LegacyJavaAppserverClient client = new RemoteLegacyJavaAppserverClient(pool);
          if (!client.connect())
          {
             return null;
          }

          return client;
       }

       /**
        * Set the {@link DataSetSDOHelper#setUnknownValuesForFields} flag.  This will allow the getter to return
        * <code>null</code> if the field is not currently set in a table row (or even if it was set to null),
        * instead of returning 0 or false for Java native types (like int and boolean).
        *
        * @param    state
        *           <code>true</code> to return null if the field is not set.
        */
       public static void setUnknownValuesForFields(boolean state)
       {
          DataSetSDOHelper.setUnknownValuesForFields(state);
       }

       /**
        * Set the {@link DataSetSDOHelper#setMergeTables} flag.  This will allow the response to be merged based
        * on the signature instead of being replaced in the dataset.
        *
        * @param    state
        *           <code>true</code> to merge the response.
        */
       public static void setMergeTables(boolean state)
       {
          DataSetSDOHelper.setMergeTables(state);
       }

       /**
        * Cancel all the requests active on this client.
        */
       public void cancelAllRequests()
       {
          if (connectionID == null)
          {
             return;
          }

          JavaClientAppServerWork work = (worker) ->
          {
             worker.getHelper().sendStop();
          };

          execute(work);
       }

       /**
        * Check if the {@link #pool} is initialized.
        *
        * @return   See above.
        */
       public boolean isInitialized()
       {
          return pool.isInitialized();
       }

       /**
        * Execute the connection to the worker.
        *
        * @return   <code>true</code> if it connected.
        */
       public boolean connect()
       {
          if (connectionID != null)
          {
             return true;
          }

          boolean ran = pool.dispatch(null, (LegacyServiceWorker worker) ->
          {
             // get the connection ID on which this request will be processed
             this.connectionID = worker.getHelper().getConnectionID();
          },
          null);

          return ran;
       }

       /**
        * 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    procedure
        *           The target external program.  Must have no parameters defined.
        * @param    paramArray
        *           The parameter array.
        */
       public void invokeWithArgs(String procedure, LegacyJavaAppserverParameter[] paramArray)
       {
          RuntimeException[] err = new RuntimeException[1];

          JavaClientAppServerWork work = (worker) ->
          {
             try
             {
                String modes = paramArray == null ? null : getArgumentModes(paramArray);
                Object[] args = paramArray == null ? new Object[0] : getArguments(paramArray);
                int[] argTypes = new int[args.length];
                for (int i = 0; i < argTypes.length; i++)
                {
                   argTypes[i] = paramArray[i].getType();
                }

                AppServerInvocationResult result =
                   worker.getHelper().invokeWithArgs(procedure, argTypes, modes, args);

                postProcessArguments(paramArray, args);
             }
             catch (ConditionException ce)
             {
                err[0] = ce;
             }
             catch (Throwable t)
             {
                err[0] = new RuntimeAppserverError(t);
             }
          };

          execute(work);

          if (err[0] != null)
          {
             throw err[0];
          }
       }

       /**
        * Delete the specified procedure handle.
        *
        * @param    h
        *           The procedure handle.
        *
        * @throws   ConditionException
        *           In case the remote call has thrown an condition (STOP, QUIT, ERROR).
        * @throws   RuntimeAppserverError
        *           In case the remote call has thrown an unexpected error.
        */
       public void deleteProcedure(handle h)
       throws ConditionException,
              RuntimeAppserverError
       {
          RuntimeException[] err = new RuntimeException[1];

          JavaClientAppServerWork work = (worker) ->
          {
             try
             {
                ProxyProcedureWrapper proc = (ProxyProcedureWrapper) h.getResource();
                worker.getHelper().deleteProcedure(proc);
                worker.removeProcedure(sessionID, proc);
             }
             catch (ConditionException ce)
             {
                err[0] = ce;
             }
             catch (Throwable t)
             {
                err[0] = new RuntimeAppserverError(t);
             }
          };

          execute(work);

          if (err[0] != null)
          {
             throw err[0];
          }
       }

       /**
        * Run the specified internal function in the given proxy external program.
        *
        * @param    proxy
        *           A proxy external program obtained via {@link #runPersistentProcedure}.
        * @param    function
        *           The internal function name.
        * @param    paramArray
        *           The parameters.
        *
        * @return   The function's returned value.
        *
        * @throws   ConditionException
        *           In case the remote call has thrown an condition (STOP, QUIT, ERROR).
        * @throws   RuntimeAppserverError
        *           In case the remote call has thrown an unexpected error.
        */
       public Object runFunction(handle proxy, String function, LegacyJavaAppserverParameter... paramArray)
       {
          RuntimeException[] err = new RuntimeException[1];

          BaseDataType[] result = new BaseDataType[1];

          JavaClientAppServerWork work = (worker) ->
          {
             try
             {
                String modes = paramArray == null ? null : getArgumentModes(paramArray);
                Object[] args = paramArray == null ? new Object[0] : getArguments(paramArray);
                BaseDataType res = worker.getHelper().invokeFunction(new character(function), proxy, false, modes, args);
                result[0] = res == null ? null : res;
                postProcessArguments(paramArray, args);
             }
             catch (ConditionException ce)
             {
                err[0] = ce;
             }
             catch (Throwable t)
             {
                err[0] = new RuntimeAppserverError(t);
             }
          };

          execute(work);

          if (err[0] != null)
          {
             throw err[0];
          }

          return result[0];
       }

       /**
        * Run the specified external program in non-persistent mode.
        *
        * @param    procedure
        *           The external program name.
        * @param    paramArray
        *           The parameters.
        *
        * @return   The RETURN-VALUE.
        *
        * @throws   ConditionException
        *           In case the remote call has thrown an condition (STOP, QUIT, ERROR).
        * @throws   RuntimeAppserverError
        *           In case the remote call has thrown an unexpected error.
        */
       public String runProcedure(String procedure, LegacyJavaAppserverParameter... paramArray)
       throws ConditionException,
              RuntimeAppserverError
       {
          RuntimeException[] err = new RuntimeException[1];

          String[] result = new String[1];

          JavaClientAppServerWork work = (worker) ->
          {
             try
             {
                String modes = paramArray == null ? null : getArgumentModes(paramArray);
                Object[] args = paramArray == null ? new Object[0] : getArguments(paramArray);

                InvokeConfig cfg = new InvokeConfig(procedure);
                cfg.setArguments(args)
                   .setModes(modes);

                BaseDataType res = worker.getHelper().invoke(0, null, cfg);
                result[0] = res == null || res.isUnknown() ? null : res.toStringMessage();
                postProcessArguments(paramArray, args);
             }
             catch (ConditionException ce)
             {
                err[0] = ce;
             }
             catch (Throwable t)
             {
                err[0] = new RuntimeAppserverError(t);
             }
          };

          execute(work);

          if (err[0] != null)
          {
             throw err[0];
          }

          return result[0];
       }

       /**
        * Run the specified external program in SINGLE-RUN mode.
        *
        * @param    procedure
        *           The external program name.
        * @param    paramArray
        *           The parameters.
        *
        * @return   The RETURN-VALUE.
        *
        * @throws   ConditionException
        *           In case the remote call has thrown an condition (STOP, QUIT, ERROR).
        * @throws   RuntimeAppserverError
        *           In case the remote call has thrown an unexpected error.
        */
       public handle runSingleRunProcedure(String procedure, LegacyJavaAppserverParameter... paramArray)
       throws ConditionException,
              RuntimeAppserverError
       {
          // TODO: are parameters possible?
          DeferredProgram prog = new DeferredProgram(procedure);
          prog.setSingleRun(true);
          return new handle(new DeferredProgramWrapper(prog));
       }

       /**
        * Run the specified external program in SINGLETON mode.
        *
        * @param    procedure
        *           The external program name.
        * @param    paramArray
        *           The parameters.
        *
        * @return   The RETURN-VALUE.
        *
        * @throws   ConditionException
        *           In case the remote call has thrown an condition (STOP, QUIT, ERROR).
        * @throws   RuntimeAppserverError
        *           In case the remote call has thrown an unexpected error.
        */
       public handle runSingletonProcedure(String procedure, LegacyJavaAppserverParameter... paramArray)
       throws ConditionException,
              RuntimeAppserverError
       {
          // TODO: are parameters possible?
          DeferredProgram prog = new DeferredProgram(procedure);
          prog.setSingleton(true);
          return new handle(new DeferredProgramWrapper(prog));
       }

       /**
        * Run the specified internal procedure in the given proxy external program.
        *
        * @param    proxy
        *           A proxy external program obtained via {@link #runPersistentProcedure}.
        * @param    procedure
        *           The internal procedure name.
        * @param    paramArray
        *           The parameters.
        *
        * @return   The RETURN-VALUE.
        *
        * @throws   ConditionException
        *           In case the remote call has thrown an condition (STOP, QUIT, ERROR).
        * @throws   RuntimeAppserverError
        *           In case the remote call has thrown an unexpected error.
        */
       public String runProcedure(handle proxy, String procedure, LegacyJavaAppserverParameter... paramArray)
       throws ConditionException,
              RuntimeAppserverError
       {
          RuntimeException[] err = new RuntimeException[1];

          String[] result = new String[1];

          JavaClientAppServerWork work = (worker) ->
          {
             try
             {
                String modes = paramArray == null ? null : getArgumentModes(paramArray);
                Object[] args = paramArray == null ? new Object[0] : getArguments(paramArray);
                character res;
                if (proxy.getResource() instanceof DeferredProgramWrapper)
                {
                   DeferredProgram prog = (DeferredProgram) proxy.get();
                   InvokeConfig cfg = new InvokeConfig(new character(procedure));
                   cfg.setModes(modes)
                      .setArguments(args)
                      .setSingleton(prog.isSingleton())
                      .setSingleRun(prog.isSingleRun())
                      .setFunction(false)
                      .setInHandle(proxy);

                   BaseDataType ret = worker.getHelper().invoke(0, null, cfg);
                   if (ret == null || ret.isUnknown())
                   {
                      res = new character();
                   }
                   else
                   {
                      res = new character(ret.toStringMessage());
                   }
                }
                else
                {
                   res = worker.getHelper().invoke(new character(procedure), proxy, false, modes, args);
                }
                result[0] = res == null ? null : res.toJavaType();
                postProcessArguments(paramArray, args);
             }
             catch (ConditionException ce)
             {
                err[0] = ce;
             }
             catch (Throwable t)
             {
                err[0] = new RuntimeAppserverError(t);
             }
          };

          execute(work);

          if (err[0] != null)
          {
             throw err[0];
          }

          return result[0];
       }

       /**
        * Run an external program persistent.
        *
        * @param    procedureName
        *           The procedure name.
        * @param    paramArray
        *           The parameters.
        *
        * @return   The proxy for this external program.
        *
        * @throws   ConditionException
        *           In case the remote call has thrown an condition (STOP, QUIT, ERROR).
        * @throws   RuntimeAppserverError
        *           In case the remote call has thrown an unexpected error.
        */
       public handle runPersistentProcedure(String procedureName, LegacyJavaAppserverParameter... paramArray)
       throws ConditionException,
              RuntimeAppserverError
       {
          RuntimeException[] err = new RuntimeException[1];

          handle[] res = new handle[] { new handle() };

          JavaClientAppServerWork work = (worker) ->
          {
             try
             {
                String modes = paramArray == null ? null : getArgumentModes(paramArray);
                Object[] args = paramArray == null ? new Object[0] : getArguments(paramArray);
                worker.getHelper().invokePersistent(new character(procedureName), res[0], false, modes, args);
                postProcessArguments(paramArray, args);

                worker.addProcedure(sessionID, (ProxyProcedureWrapper) res[0].getResource());
             }
             catch (ConditionException ce)
             {
                err[0] = ce;
             }
             catch (Throwable t)
             {
                err[0] = new RuntimeAppserverError(t);
             }
          };

          execute(work);

          if (err[0] != null)
          {
             throw err[0];
          }

          return res[0];
       }

       /**
        * Disconnect this client.
        */
       public void disconnect()
       {
          JavaClientAppServerWork work = (worker) ->
          {
             Set<ProxyProcedureWrapper> procedures = worker.getProcedures(sessionID);
             for (ProxyProcedureWrapper proc : procedures)
             {
                worker.getHelper().deleteProcedure(proc);
                worker.removeProcedure(sessionID, proc);
             }

             worker.removeProcedures(sessionID);
          };

          execute(work);
       }

       /**
        * Send the given task to be executed by one of the workers in the {@link #pool}.
        *
        * @param    work
        *           The work to be executed.
        *
        * @throws   RuntimeException
        *           If the request was not posted to any worker or the time allowed to execute has elapsed.
        */
       protected boolean execute(LegacyAppServerWork work)
       {
          String target = getTarget();

          boolean ran = pool.dispatch(connectionID, work, target);

          return ran;
       }

       /**
        * Set the {@link LegacyJavaAppserverParameter#setOrig orig} value, useful when mocking the
        * {@link LegacyJavaAppserverClient}.
        *
        * @param    param
        *           The parameter instance.
        * @param    orig
        *           The value assumed to be returned by the remote side.
        */
       void setParameterOrig(LegacyJavaAppserverParameter param, Object orig)
       {
          param.setOrig(orig);
       }

       /**
        * Get the target for the current web request.  If no HTTP web request is available, return 'n/a'.
        *
        * @return   See above.
        */
       private String getTarget()
       {
          HttpServletRequest request = CurrentRequestFilter.getCurrentRequest();

          String target = request != null ? request.getServletPath() : "n/a";

          return target;
       }

       /**
        * Interface for the work task to be processed by the legacy appserver worker, as prepared by the java
        * client.
        */
       private interface JavaClientAppServerWork
       extends LegacyAppServerWork
       {
          /**
           * Returns the client type associated with the task to be executed on the legacy worker.
           *
           * @return   See above.
           */
          default ClientType getClientType()
          {
             return ClientType.JAVACLIENT;
          }
       }
    }