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