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