MultiSessionClientConnection.java
/*
** Module : MultiSessionClientConnection.java
** Abstract : Connection between the appserver client and the multi-session appserver manager.
**
** Copyright (c) 2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 GBB 20250122 Created initial version.
*/
/*
** 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.main.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import java.lang.ref.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;
/** Connection between the appserver client and the multi-session appserver manager. */
public class MultiSessionClientConnection
{
/** Agent system connection id. Used for starting / shutting down agent and sessions. */
static final String AGENT_SYS_CONN_ID = ""; // always empty in OE, not unknown
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(MultiSessionClientConnection.class);
/** The connection id. */
private final String connectionId;
/** The parent connection id. */
private final MultiSessionClientConnection parentConnection;
/** Map of persistent procs by their res id. */
private final Map<String, AppserverRemoteProcedure> persistentProcs = new ConcurrentHashMap<>();
/** Connections created for persistent procs in Session-free mode. */
private final Set<MultiSessionClientConnection> childConnections = ConcurrentHashMap.newKeySet();
/** The name of the appserver the connection is for. */
private final String appserverName;
/** The session model the connection uses. */
private final AppServerSessionModel model;
/** Flag to indicate if the connection is used by a web service. */
private final boolean isForWebService;
/** The id of the session the connection is bound to. */
private Integer boundSessionId;
/** The SESSION:SERVER-CONNECTION-CONTEXT attribute. */
private String contextBlob;
/** The SESSION:SERVER-CONNECTION-BOUND-REQUEST attribute. */
private boolean serverConnectionBoundRequest;
/** Flag to suppress legacy OO errors on the current connection. */
private boolean disableLegacyErrors;
/** Flag to indicate the connection is terminating. */
private final AtomicBoolean isDisconnecting = new AtomicBoolean(false);
/** The task wrapper to be running on this connection. */
private CancellableCompositeTask<AppServerInvocationResult> wrapperTask;
/** The weak reference to the MSA task currently running on this connection. */
private WeakReference<MultiSessionAppserver.MsaTask> msaTaskRef;
/** The currently running procedure / program. */
private String currentlyRunningProgram;
/** The time the currently running procedure / program started. */
private Date currentlyRunningProgramStartTime;
/** The date and time of the last use of the connection. */
private Date lastUseTime = new Date();
/**
* Package-private constructor.
*
* @param connectionId
* The connection id.
* @param parentConnection
* The parent connection, if this is a child connection.
* @param appserverName
* The name of the appserver the connection is for.
* @param isSessionFree
* Flag indicating if the session model is Session-free.
* @param isForWebService
* The connection is used by a web service.
*/
MultiSessionClientConnection(String connectionId,
MultiSessionClientConnection parentConnection,
String appserverName,
boolean isSessionFree,
boolean isForWebService)
{
this.connectionId = connectionId;
this.parentConnection = parentConnection;
this.appserverName = appserverName;
this.model = isSessionFree ? AppServerSessionModel.SESSION_FREE : AppServerSessionModel.SESSION_MANAGED;
this.isForWebService = isForWebService;
}
/**
* Checks if the argument object represents the same task, identified by the connectionId.
*
* @param o
* The object to compare to.
*
* @return <code>true</code> if it's the same instance or connectionId is the same.
* <code>false</code> otherwise.
*/
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
MultiSessionClientConnection that = (MultiSessionClientConnection) o;
return connectionId.equals(that.connectionId);
}
/**
* Returns the hash of the fields uniquely identifying the object.
*
* @return See above.
*/
@Override
public int hashCode()
{
return Objects.hash(connectionId);
}
/**
* Returns <code>true</code> if there is any type of task / program registered to be currently running
* on that connection, <code>false</code> otherwise.
*
* @return See above.
*/
boolean isInUse()
{
return currentlyRunningProgram != null ||
(msaTaskRef != null && msaTaskRef.get() != null) ||
wrapperTask != null;
}
/**
* Getter for {@link #connectionId}.
*
* @return See above.
*/
String getId()
{
return connectionId;
}
/**
* Getter for {@link #parentConnection}.
*
* @return See above.
*/
MultiSessionClientConnection getParentConnection()
{
return parentConnection;
}
/**
* Getter for {@link #isForWebService}.
*
* @return See above.
*/
boolean isForWebService()
{
return isForWebService;
}
/**
* Returns the last use time of the main and child connections.
*
* @return See above.
*/
Date getLastUseTime()
{
Date latestChildConnectionUseTime = new Date(0);
for (MultiSessionClientConnection childConnection : childConnections)
{
if (childConnection.getLastUseTime().getTime() > latestChildConnectionUseTime.getTime())
{
latestChildConnectionUseTime = childConnection.getLastUseTime();
}
}
return latestChildConnectionUseTime.getTime() > lastUseTime.getTime() ?
latestChildConnectionUseTime :
lastUseTime;
}
/**
* Sets {@link #lastUseTime} to the current date.
*/
void setLastUseTime()
{
lastUseTime = new Date();
}
/**
* Getter for {@link #appserverName}.
*
* @return See above.
*/
String getAppserverName()
{
return appserverName;
}
/**
* Returns <code>true</code> if the connection is Session-free, otheriwse <code>false<code/>.
*
* @return See above.
*/
boolean isSessionFree()
{
return model == AppServerSessionModel.SESSION_FREE;
}
/**
* Getter for {@link #model}.
*
* @return See above.
*/
public AppServerSessionModel getModel()
{
return model;
}
/**
* Getter for {@link #boundSessionId}.
*
* @return See above.
*/
Integer getBoundSessionId()
{
return boundSessionId;
}
/**
* Sets the appserver session id the connection is bound to.
*
* @param sessionId
* The appserver session id.
*/
void setBoundSessionId(Integer sessionId)
{
this.boundSessionId = sessionId;
}
/**
* Returns if the connection is bound to an appserver session.
*
* @return See above.
*/
boolean isBound()
{
return boundSessionId != null;
}
/**
* Get the SERVER-CONNECTION-CONTEXT attribute of this session.
*
* @return See above.
*/
String getServerConnectionContext()
{
return contextBlob;
}
/**
* Sets the SERVER-CONNECTION-CONTEXT attribute of this session.
*
* @param value
* The value of the free text to be saved in the connection as context.
*/
void setServerConnectionContext(String value)
{
contextBlob = value;
}
/**
* Set the SESSION:SERVER-CONNECTION-BOUND-REQUEST attribute.
*
* @param serverConnectionBoundRequest
* The SESSION:SERVER-CONNECTION-BOUND-REQUEST value.
*/
void setServerConnectionBoundRequest(boolean serverConnectionBoundRequest)
{
this.serverConnectionBoundRequest = serverConnectionBoundRequest;
}
/**
* Returns the value of the SESSION:SERVER-CONNECTION-BOUND-REQUEST attribute.
*
* @return See above.
*/
boolean getServerConnectionBoundRequest()
{
return serverConnectionBoundRequest;
}
/**
* Disable legacy OO errors thrown on this connection.
*/
void disableLegacyErrors(boolean disableLegacyErrors)
{
this.disableLegacyErrors = disableLegacyErrors;
}
/**
* Check if the current connection should suppress legacy OO errors.
*
* @return See above.
*/
boolean hasLegacyErrorsDisabled()
{
return disableLegacyErrors;
}
/**
* Returns <code>true</code> if the connection is in the process of being disconnected,
* <code>false</code> otherwise.
*
* @return See above.
*/
boolean isDisconnecting()
{
return isDisconnecting.get();
}
/**
* Sets the state of the connection.
*
* @param isDisconnecting
* The new state of the connection.
*/
void setDisconnecting(boolean isDisconnecting)
{
this.isDisconnecting.set(isDisconnecting);
}
/**
* Sets the currently running MSA task.
*
* @param msaTask
* The currently running MSA task.
*/
void setCurrentlyRunningTask(MultiSessionAppserver.MsaTask msaTask)
{
this.msaTaskRef = new WeakReference<>(msaTask);
}
/**
* Stops the current task. First checks if the wrapper task is known, if not, then checks for the MSA task.
*/
void stopCurrentTask()
{
if (wrapperTask != null)
{
cancelTaskSync(connectionId);
return;
}
MultiSessionAppserver.MsaTask msaTask = msaTaskRef.get();
if (msaTask != null)
{
msaTask.sendStop();
}
}
/**
* Sets the name of the currently running program.
*
* @param currentlyRunningProgram
* The name of the currently running program.
*/
void setCurrentlyRunningProgram(String currentlyRunningProgram)
{
if (currentlyRunningProgram == null)
{
this.currentlyRunningProgram = null;
this.currentlyRunningProgramStartTime = null;
}
else
{
this.currentlyRunningProgram = currentlyRunningProgram;
this.currentlyRunningProgramStartTime = new Date();
}
setLastUseTime();
}
/**
* Getter for {@link #currentlyRunningProgram}.
*
* @return See above.
*/
String getCurrentlyRunningProgram()
{
return currentlyRunningProgram;
}
/**
* Getter for {@link #currentlyRunningProgramStartTime}.
*
* @return See above.
*/
Date getCurrentlyRunningProgramStartTime()
{
return currentlyRunningProgramStartTime;
}
/**
* Adds a persistent procedure mapped by its resource id.
*
* @param extProg
* The external program handle.
*/
void addPersistentProc(handle extProg)
{
AppserverRemoteProcedure proc = new AppserverRemoteProcedure(extProg);
persistentProcs.put(proc.getExtProgResourceId(), proc);
}
/**
* Deletes the procedure handle from the session and its reference from the client connection.
* Should be running in the correct context {@link #boundSessionId}.
*
* @param procResId
* The ID of a remote procedure which needs to be deleted.
*/
void deletePersistentProc(String procResId)
{
AppserverRemoteProcedure remote = persistentProcs.remove(procResId);
if (remote != null)
{
ExternalProgramWrapper proc = remote.getExtProg();
if (AppserverPackageExports.isKnownResource(proc))
{
proc.delete();
}
}
}
/**
* Deletes the remote object, specified by the resource ID and its reference from the client connection.
* Should be running in the correct context {@link #boundSessionId}. This must be called at the same
* time when the local proxy object is deleted.
*
* @param objResId
* The ID of a remote object which needs to be deleted.
*/
void deletePersistentObject(String objResId)
{
AppserverRemoteProcedure remote = persistentProcs.remove(objResId);
if (remote != null)
{
ExternalProgramWrapper proc = remote.getExtProg();
if (AppserverPackageExports.isKnownResource(proc))
{
AppserverPackageExports.deleteObject((BaseObject) proc.get());
}
}
}
/**
* Finds the program wrapper for the persistent procedure by its resource id.
*
* @param persistentProcResId
* The resource if of the persistent procedure.
*
* @return The program wrapper for the persistent procedure.
*/
ExternalProgramWrapper getProcWrapper(String persistentProcResId)
{
AppserverRemoteProcedure remote = persistentProcs.get(persistentProcResId);
return remote == null ? null : remote.getExtProg();
}
/**
* Finds the remote procedure by its resource id.
*
* @param persistentProcResId
* The resource if of the persistent procedure.
*
* @return The remote procedure.
*/
AppserverRemoteProcedure getProc(String persistentProcResId)
{
return persistentProcs.get(persistentProcResId);
}
/**
* Returns all persistent procedures added to this connection.
*
* @return See above.
*/
Collection<AppserverRemoteProcedure> getPersistentProcs()
{
return persistentProcs.values();
}
/**
* Returns the name of the persistent procedure by searching by its resource id in the current
* connection and in its child connections.
*
* @param persistentProcResId
* The resource if of the persistent procedure.
*
* @return See above.
*/
String getPersistentProcName(String persistentProcResId)
{
AppserverRemoteProcedure proc = persistentProcs.get(persistentProcResId);
if (proc != null)
{
return proc.getProcedureName();
}
for (MultiSessionClientConnection connection : childConnections)
{
String procName = connection.getPersistentProcName(persistentProcResId);
if (procName != null)
{
return procName;
}
}
return null;
}
/**
* Adds a child connection.
*
* @param childConnection
* The child connection to be added.
*/
void addChildConnection(MultiSessionClientConnection childConnection)
{
childConnections.add(childConnection);
setLastUseTime();
}
/**
* Returns the connection the persistent proc has been added to.
*
* @param persistentProcResId
* The resource if of the persistent procedure.
*
* @return See above.
*/
MultiSessionClientConnection findConnectionFor(String persistentProcResId)
{
AppserverRemoteProcedure proc = persistentProcs.get(persistentProcResId);
if (proc != null)
{
return this;
}
for (MultiSessionClientConnection connection : childConnections)
{
MultiSessionClientConnection foundConnection = connection.findConnectionFor(persistentProcResId);
if (foundConnection != null)
{
return foundConnection;
}
}
return null;
}
/**
* Returns all child connections.
*
* @return See above.
*/
Collection<MultiSessionClientConnection> getChildConnections()
{
return new HashSet<MultiSessionClientConnection>(childConnections);
}
/**
* Returns the number of persistent procedure running on this connection.
*
* @return See above.
*/
boolean hasPersistentProc()
{
return persistentProcs.size() > 0;
}
/**
* Check if the connection has the given procedure added.
*
* @param referent
* A persistent procedure referent.
*
* @return <code>true</code> if the persistent procedure is known to the current running connection.
*/
boolean hasPersistentProc(Object referent)
{
return persistentProcs.values()
.stream()
.anyMatch(appsRemProc -> appsRemProc.getExtProg().get().equals(referent));
}
/**
* Adds the wrapper task for the currently running request.
*
* @param task
* The wrapper task for the currently running request.
*/
void addTask(CancellableCompositeTask<AppServerInvocationResult> task)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Adding a new CancellableCompositeTask with id " + task.getTaskId() +
" of type " + task.getType() +
" to connection " + connectionId +
" on thread " + Thread.currentThread());
}
this.wrapperTask = task;
setLastUseTime();
}
/**
* Removes the wrapper task for the currently running request.
*
* @param task
* The wrapper task for the currently running request.
*/
void removeTask(CancellableCompositeTask<AppServerInvocationResult> task)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Removing CancellableCompositeTask with id " + task.getTaskId() +
" of type " + task.getType() +
" from connection " + connectionId +
" on thread " + Thread.currentThread());
}
this.wrapperTask = null;
setLastUseTime();
}
/**
* Stops all wrapper tasks running on this connection and its child connections.
*/
void cancelTasks()
{
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Cancelling tasks of connection " + connectionId + " on thread " + Thread.currentThread());
}
cancelTaskSync(connectionId);
for (MultiSessionClientConnection childConnection : childConnections)
{
cancelTaskSync(childConnection.getId());
}
}
/**
* Stops the wrapper task running on this connection.
*/
private void cancelTaskSync(String connectionId)
{
if (wrapperTask == null)
{
return;
}
AtomicBoolean runningSupplier = wrapperTask.getRunningSupplier();
if (runningSupplier == null)
{
cancelTask(wrapperTask, runningSupplier, connectionId);
return;
}
synchronized (runningSupplier)
{
cancelTask(wrapperTask, runningSupplier, connectionId);
}
}
/**
* Stops the task if it's already stared, cancels otherwise.
*
* @param task
* The wrapper task for the currently running request.
* @param runningSupplier
* The thread-safe supplier providing info if the wrapped MSA task is running.
* @param connectionId
* The connection id.
*/
private static void cancelTask(CancellableCompositeTask task,
AtomicBoolean runningSupplier,
String connectionId)
{
if (runningSupplier.get())
{
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Interrupting task with id " + task.getTaskId() +
" of type " + task.getType() +
" on connection " + connectionId +
" on thread " + Thread.currentThread());
}
task.sendStop();
}
else
{
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Cancelling task with id " + task.getTaskId() +
" of type " + task.getType() +
" on connection " + connectionId +
" on thread " + Thread.currentThread());
}
task.cancel(true);
}
}
}