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