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