AsyncRequestImpl.java

/*
** Module   : AsyncRequestImpl.java
** Abstract : implementation of the async request resource.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 CA  20130813 Created initial version.
** 002 CA  20130927 Resource type is computed from the annotation. Removed setUnknown(false) call.
** 003 VIG 20131126 Added LAST-EVENT:LABEL support for PROCEDURE-COMPLETE event
** 004 CA  20131220 Changed to support both appserver and web service async requests.
** 005 GES 20140116 Fixed some member references since the resource moved between classes.
** 006 OM  20160712 Fixed synchronization issue.
** 007 CA  20190628 eventProcedureContext is optional via 'null'.
** 008 CA  20200427 Added REQUEST-INFO and RESPONSE-INFO support.
** 009 CA  20200827 Reworked asynchronous invocations to perform all context-local work on the Conversation
**                  thread, and let only the actual invocation be performed in an AssociatedThread.
**     OM  20201120 Changed method name to match no-bean prefixes buffer conversion.
**     SVL 20210202 Assign return value to an invocation result only if the value was set during this call.
**     CA  20210204 The requestInfo and responseInfo must be vars.
**     OM  20210309 Removed deprecation warnings.
**     CA  20210405 Fixed OUTPUT arguments passed to the PROCEDURE-COMPLETE event.
**     SVL 20230108 Improved performance by replacing some "for-each" loops with indexed "for" loops.
**     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** 010 CA  20240910 A PROCEDURE-COMPLETE event can be processed if even if the server gets disconnected, as 
**                  long as the server resource doesn't get deleted.
** 011 GBB 20250403 Adding fields for the session model and the id of the connection running the request.
*/

/*
** 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 com.goldencode.p2j.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.client.event.*;
import com.goldencode.util.*;

/**
 * Implements the async request resource. Beside adding support for its attributes, the async
 * resource is also responsible of initiating the async call and raising the PROCEDURE-COMPLETE
 * event.
 * <p>
 * As some the mutable fields of this class can be modified by more than one thread, they are
 * marked as {@code volatile}.
 */
public class AsyncRequestImpl
extends HandleChain
implements AsyncRequest,
           Runnable
{
   /** A handle to the SERVER resource. */
   private final handle server = new handle();
   
   /** The name of the EVENT-PROCEDURE. */
   private String eventProcedure = null;
   
   /** The context of the EVENT-PROCEDURE. */
   private WrappedResource eventProcedureContext;
   
   /** The proxy procedure, if the remote call is for an internal procedure. */
   private WrappedResource persistentProcedure;
   
   /** The name of the target procedure. */
   private String procedureName = null;
   
   /** The procedure's arguments. */
   private final Object[] args;
   
   /** The modes for the procedure's arguments. */
   private String modes;
   
   /** The result from the remote call. */
   private RemoteInvocationResult result = null;
   
   /** The appserver session model used by the connection running this request. */
   private AppServerSessionModel appServerConnectionModel;
   
   /**
    * Flag indicating that the async call has completed (will be {@code false} if the call was 
    * cancelled.
    */
   private volatile boolean complete = false;
   
   /** Flag indicating that the async call has been cancelled. */
   private volatile boolean cancelled = false;
   
   /** Flag indicating that the call was terminated with a QUIT condition. */
   private volatile boolean quit = false;
   
   /** Flag indicating that the call was terminated with a STOP condition. */
   private volatile boolean stop = false;
   
   /** Flag indicating that the call was terminated with an ERROR condition. */
   private volatile boolean error = false;
   
   /** Flag indicating if the resource is valid. */
   private volatile boolean valid = true;
   
   /** The ID of the remote request being executed. */
   private volatile int requestId = -1;
   
   /**
    * List of listeners which need to receive notifications when the call is started or finished.
    */
   private ArrayList<AsyncRequestListener> listeners = new ArrayList<>();
   
   /** A worker indicating what will be invoked. */
   private Runnable request;

   /** The reference to the REQUEST-INFO attribute. */
   private object<? extends OerequestInfo> requestInfo = new ObjectVar<>(OerequestInfo.class);

   /** The reference to the RESPONSE-INFO attribute. */
   private object<? extends OerequestInfo> responseInfo = new ObjectVar<>(OerequestInfo.class);

   /** Flag indicating if this is a web service call. */
   private boolean webService;

   /** Flag indicating if the request invoked on another thread or immediately. */
   private boolean async = false;
   
   /** The async action to execute. */
   private Runnable asyncAction = null;

   /**
    * The number of passes for the {@link #execute} method - 1 is on the Conversation thread, 2 is on a 
    * separate thread.
    */
   private volatile int passCount = 0;
   
   /** For appserver calls, the helper for the remote connection. */
   private AppServerHelper helper = null;
   
   /** The id of the client connection running the async request. */
   private String connectionId;

   /**
    * Create a new async request resource, by specifying the attributes related to the async
    * call definition.
    * 
    * @param    server
    *           The server used to invoke the remote call. May be a {@code SERVER} or the
    *           {@code SESSION} resource.
    * @param    request
    *           The request which will execute the procedure call.
    * @param    eventProcedure
    *           The event procedure name.
    * @param    eventProcedureContext
    *           The context of the event procedure.
    * @param    procedureName
    *           The name of the procedure to be invoked.
    * @param    persistentProcedure
    *           The handle to the proxy procedure to which the target procedure belongs. Will
    *           be unknown if this is not a remote call.
    * @param    modes
    *           The modes for the procedure's arguments.
    * @param    args
    *           The procedure's arguments.
    */
   public AsyncRequestImpl(handle    server,
                           Runnable  request,
                           character eventProcedure,
                           handle    eventProcedureContext,
                           character procedureName,
                           handle    persistentProcedure,
                           String    modes,
                           Object... args)
   {
      this.webService = server.getResource() instanceof Server && 
                        ((ServerImpl) server.getResource()).isWebService();
      if (!webService)
      {
         requestInfo.assign(ObjectOps.newInstance(OerequestInfo.class));
         if (server.getResource() instanceof Server)
         {
            helper = (AppServerHelper) ((ServerImpl) server.getResource()).getServerHelper();
         }
      }
      
      this.server.assign(server);
      this.request = request;
      if (eventProcedure != null)
      {
         this.eventProcedure = eventProcedure.getValue();
         if (eventProcedureContext == null)
         {
            this.eventProcedureContext = ProcedureManager.thisProcedure().getResource();
         }
         else
         {
            this.eventProcedureContext = eventProcedureContext.getResource();
         }
      }
      this.persistentProcedure = persistentProcedure.getResource();
      this.procedureName = procedureName.getValue();
      this.modes = modes;
      // arguments need special processing; copies are made of all BDT instances; TABLE
      // parameters remain intact
      this.args = new Object[args.length];
      for (int i = 0; i < args.length; i++)
      {
         if (args[i] instanceof BaseDataType)
         {
            this.args[i] = ((BaseDataType) args[i]).duplicate();
         }
      }
   }
   
   /**
    * Get the parameter modes for this async request.
    * 
    * @return   See above.
    */
   public String getModes()
   {
      return this.modes;
   }
   
   /**
    * Get the arguments for this async requests.
    * 
    * @return   See above.
    */
   public Object[] getArgs()
   {
      return this.args;
   }
   
   /**
    * Returns the state of the ERROR attribute
    *
    * @return    Current state of the ERROR attribute.
    */
   @Override
   public logical error()
   {
      return !complete ? new logical() : new logical(error);
   }
   
   /**
    * Setter for ERROR attribute.
    *
    * @param    value
    *           The new value of ERROR attribute.
    */
   @Override
   public void error(boolean value)
   {
      readOnlyError("ERROR", handle.UNKNOWN_ARGUMENT);
   }
   
   /**
    * Setter for ERROR attribute.
    *
    * @param    value
    *           The new value of ERROR attribute.
    */
   @Override
   public void error(logical value)
   {
      readOnlyError("ERROR", handle.UNKNOWN_ARGUMENT);
   }
   
   /**
    * Reports if this object is valid for use (has not been deleted).
    *
    * @return   {@code true} if we are valid (can be used).
    */
   @Override
   public boolean valid()
   {
      return valid;
   }

   /**
    * Returns a handle to the app server in which the procedure is ran.
    * 
    * @return   See above
    */
   @Override
   public handle getServerHandle()
   {
      return new handle(server);
   }

   /**
    * Get the {@code CANCELLED} attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public logical isCancelled()
   {
      return new logical(cancelled);
   }

   /**
    * Get the {@code CANCELLED} attribute of this async request, as a Java boolean.
    * 
    * @return   See above.
    */
   public boolean _isCancelled()
   {
      return cancelled;
   }

   /**
    * Get the {@code COMPLETE} attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public logical isComplete()
   {
      return new logical(complete);
   }

   /**
    * Get the {@code COMPLETE} attribute of this async request, as a Java boolean.
    * 
    * @return   See above.
    */
   public boolean _isComplete()
   {
      return complete;
   }

   /**
    * Get the {@code EVENT-PROCEDURE} attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public character getEventProcedure()
   {
      return new character(eventProcedure);
   }

   /**
    * Get the {@code EVENT-PROCEDURE-CONTEXT} attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public handle getEventProcedureContext()
   {
      return new handle(eventProcedureContext);
   }

   /**
    * Get the {@code PERSISTENT-PROCEDURE} attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public handle getPersistentProcedure()
   {
      return new handle(persistentProcedure);
   }

   /**
    * Get the {@code PROCEDURE-NAME} attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public character getProcedureName()
   {
      return new character(procedureName);
   }

   /**
    * Get the REQUEST-INFO legacy attribute.
    * 
    * @return   See above.
    */
   @Override
   public object<? extends OerequestInfo> getRequestInfo()
   {
      return new object<>(requestInfo);
   }
   
   /**
    * Get the RESPONSE-INFO legacy attribute.
    * 
    * @return   See above.
    */
   @Override
   public object<? extends OerequestInfo> getResponseInfo()
   {
      return new object<>(responseInfo);
   }

   /**
    * Get the {@code QUIT} attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public logical isQuit()
   {
      return new logical(quit);
   }

   /**
    * Get the {@code STOP} attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public logical isStop()
   {
      return new logical(stop);
   }

   /**
    * Invoke the event procedure for this async request, if used.
    */
   public void run()
   {
      if (!server._isValid())
      {
         // server has been deleted
         return;
      }
      
      KeyReader.setLabelWorker(Keyboard.PROCEDURE_COMPLETE);

      // change the RETURN-VALUE as set by this call
      boolean nullResult = (result == null);
      if (!nullResult)
      {
         if (result.setReturnValue())
         {
            Object res = result.getResult();
            if (res != null)
            {
               ControlFlowOps.setReturnValue(new character((BaseDataType) res));
            }
         }
         
         if (helper != null)
         {
            helper.restoreResponse((AppServerInvocationResult) result, responseInfo);
         }
      }

      // set the QUIT/STOP/ERROR flags and the details about any raised ERRORs
      Throwable cause = nullResult ? null : result.getError();
      while (cause != null)
      {
         if (cause instanceof QuitConditionException)
         {
            setQuit();
            break;
         }
         else if (cause instanceof StopConditionException)
         {
            setStop();
            break;
         }
         else if (cause instanceof ErrorConditionException)
         {
            setError();
            
            ErrorConditionException err = (ErrorConditionException) cause;
            if (err.getCause() instanceof NumberedException)
            {
               NumberedException ne = (NumberedException) err.getCause();
               ErrorManager.addError(ne.getNumber(), ne.getMessage(), true);
            }

            break;
         }

         cause = cause.getCause();
      }
      
      // no further processing is needed if no event procedure is set
      if (eventProcedure == null)
      {
         return;
      }

      List<Object> eventArgsList = new ArrayList<>();
      Object[] eventArgs = new Object[0];
      String eventModes = null;
      if (modes == null)
      {
         // get the resolved modes, if not explicitly passed at the call
         modes = nullResult ? null : result.getModes();
      }
      
      if (modes != null)
      {
         // TODO: how are tables/datasets transferred from the remote side to the event procedure?
         boolean failed = error || quit || stop || cancelled;
         
         for (int i = 0; i < modes.length(); i++)
         {
            char mode = modes.charAt(i);
            if (InternalEntry.isOutputMode(mode))
            {
               Object arg = result.getArguments()[i];
               // if the request did not complete OK, pass an unknown, but only for BDTs
               eventArgsList.add(failed && arg instanceof BaseDataType ? new unknown() 
                                                                       : arg);
            }
         }
         
         eventModes = StringHelper.repeatChar(InternalEntry.INPUT_MODE, eventArgsList.size());
         eventArgs = eventArgsList.toArray();
      }
   
      // invoke the event procedure
      try
      {
         SelfManager.pushSelf(new handle(this));
         ControlFlowOps.invokeInWithMode(eventProcedure,
                                         new handle(eventProcedureContext),
                                         eventModes,
                                         eventArgs);
      }
      finally
      {
         SelfManager.popSelf();
      }
   }

   /**
    * Returns {@link #connectionId}.
    *
    * @return   See above.
    */
   public String getConnectionId()
   {
      return connectionId;
   }

   /**
    * Returns {@link #appServerConnectionModel}.
    *
    * @return   See above.
    */
   public AppServerSessionModel getAppServerConnectionModel()
   {
      return appServerConnectionModel;
   }

   /**
    * Check if the request can be executed immediately or it needs to be async.
    * 
    * @return   The state of the {@link #async} flag.
    */
   boolean isAsync()
   {
      return async;
   }
   
   /**
    * Set the async action to be executed for this request.
    * 
    * @param    action
    *           The async action.
    */
   void setAsyncAction(Runnable action)
   {
      this.asyncAction = action;
   }
   
   /**
    * Set the ID of this remote request.
    * 
    * @param    reqId
    *           The ID of the request.
    */
   void setRequestId(int reqId)
   {
      this.requestId = reqId;
   }
   
   /**
    * Set the ID of this remote request.
    *
    * @param    connectionId
    *           The ID of the request.
    */
   void setConnectionId(String connectionId)
   {
      this.connectionId = connectionId;
   }

   /**
    * Called when the async call has finished normally (not with a cancel).
    */
   void complete()
   {
      this.complete = true;
      notifyListeners(false);
   }
   
   /**
    * Called when the async call was cancelled by the user.
    */
   void cancelled()
   {
      this.cancelled = true;
      notifyListeners(false);
   }

   /**
    * Mark this async call as having ended with a STOP condition.
    */
   void setStop()
   {
      this.stop = true;
   }

   /**
    * Mark this async call as having ended with a QUIT condition.
    */
   void setQuit()
   {
      this.quit = true;
   }

   /**
    * Mark this async call as having ended with a ERROR condition.
    */
   void setError()
   {
      this.error = true;
   }
   
   /**
    * Stop this async request by sending the STOP condition to the remote side. This will be
    * used only in cases when the async call is cancelled by the user.
    */
   void stop()
   {
      if (requestId == -1)
      {
         // the request was not yet started, prevent from starting it
         complete();
         setStop();
         return;
      }

      // send a STOP condition to the remote side
      ServerImpl s = (ServerImpl) server.get();
      if (!(server._isValid() && s._connected()))
      {
         // server is disconnected
         complete();
         return;
      }
      
      s.getServerHelper().sendStop(requestId);

      // wait for it to complete
      while (!complete)
      {
         synchronized (this)
         {
            try
            {
               this.wait();
            }
            catch (InterruptedException e)
            {
               break;
            }
         }
      }
   }

   /**
    * Set the result from the {@link #request} execution.
    * 
    * @param    result
    *           The result.
    */
   void setResult(RemoteInvocationResult result)
   {
      if (result instanceof AppServerInvocationResult)
      {
         AppServerInvocationResult appserverResult = (AppServerInvocationResult) result;
         this.appServerConnectionModel = appserverResult.getConnectionModel();
      }
      this.result = result;
   }
   
   /**
    * Register a new listener for notifications about the state of the async call.
    * 
    * @param    listener
    *           A listener instance.
    */
   void registerListener(AsyncRequestListener listener)
   {
      listeners.add(listener);
   }
   
   /**
    * Notify all listeners that the async call is about to start.
    */
   void notifyStart()
   {
      notifyListeners(true);
   }

   /**
    * Execute the async request. This will either raise the PROCEDURE-COMPLETE event or will
    * invoke the EVENT-PROCEDURE immediately.
    * 
    * @param    async
    *           Flag indicating if the request will be executed in this thread or in another thread.
    */
   void execute(boolean async)
   {
      // check if the request was not cancelled (requests which are removed from queue and are
      // pending to be executed are marked as complete plus the STOP attribute to true
      if (complete)
      {
         return;
      }

      // execute the remote call. this will be either async or immediately, depending on the 'async' flag.
      this.async = async;
      
      try
      {
         passCount++;
         request.run();
         
         if (asyncAction != null)
         {
            // we have an async action to execute - post 'this' to the server's invoker and replace the 
            // 'request' with the async action
            request = asyncAction;
            asyncAction = null;
            
            ((ServerImpl) server.getResource()).invokeAsync(this);
         }
      }
      catch (Throwable t)
      {
         // treat any conditions
         Throwable cause = t;
         while (cause != null && !(cause instanceof ConditionException))
         {
            cause = t.getCause();
         }
         
         if (cause != null)
         {
            if (cause instanceof StopConditionException)
            {
               setStop();
            }
            else if (cause instanceof QuitConditionException)
            {
               setQuit();
            }
            else if (cause instanceof ErrorConditionException)
            {
               setError();
            }
         }
         
         // delete the ASYNC handle
         if (passCount == 1 && valid)
         {
            delete();
         }
      }
      finally
      {
         if (!async)
         {
            if (!_isCancelled())
            {
               complete();
            }

            if (passCount == 1)
            {
               // execute the event now, we are on the Conversation thread
               run();

               // ensure any WAIT-FOR listening for this event is terminated
               ServerEvent serverEvent = new ServerEvent(handle.resourceId(this),
                                                         Keyboard.PROCEDURE_COMPLETE,
                                                         null);
               LogicalTerminal.postServerEvent(serverEvent);
            }
            else
            {
               // this must be executed on the Conversation's context, so we can post the event
               ServerEvent serverEvent = new ServerEvent(handle.resourceId(this),
                                                         Keyboard.PROCEDURE_COMPLETE,
                                                         this);
               LogicalTerminal.postServerEvent(serverEvent);
            }
            
            // notify that the request has finished.
            synchronized (this)
            {
               this.notify();
            }
         }
      }
   }
   
   /**
    * The async resource can be deleted only if it has been completed or cancelled.
    */
   @Override
   protected boolean resourceDelete()
   {
      if (!(complete || cancelled))
      {
         final String msg = "Asynchronous request  has not completed.  Cannot DELETE";
         ErrorManager.recordOrThrowError(8978, msg);
         return false;
      }
      
      this.valid = false;
      
      if (requestInfo._isValid())
      {
         ObjectOps.delete(requestInfo);
      }
      if (responseInfo._isValid())
      {
         ObjectOps.delete(responseInfo);
      }

      return true;
   }
   
   /**
    * As this resource supports the name attribute in R/W mode, return {@code false}
    * 
    * @return   always {@code false}
    */
   @Override
   protected boolean hasNameReadOnly()
   {
      return false;
   }
   
   /**
    * Notify the listeners about the invocation request state (start or finish).
    * 
    * @param    start
    *           {@code true} to indicate invocation start, {@code false} to notify for
    *           invocation finish.
    */
   private void notifyListeners(boolean start)
   {
      for (int i = 0; i < listeners.size(); i++)
      {
         AsyncRequestListener listener = listeners.get(i);
         if (start)
         {
            listener.notifyStart();
         }
         else
         {
            listener.notifyFinish();
         }
      }
   }
}