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