# ServerImpl.java

    /*
    ** Module   : ServerImpl.java
    ** Abstract : A class that implements the app server specific method and  attribute APIs.
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 CS  20130211 Created initial version.
    ** 002 CA  20130221 Added resourceDelete stub. setType was renamed to setResourceType.
    ** 003 OM  20130304 Refactored isValid and isUnknown of WrappedResource to valid and unknown.
    ** 004 CS  20130321 Added missing versions of connect() method.
    ** 005 CA  20130529 Added runtime support for appservers.
    ** 006 CA  20130822 Added support for async requests.
    ** 007 CA  20130908 Moved connect-related code to ConnectHelper.
    ** 008 CA  20130927 Resource type is computed from the annotation. Removed setUnknown(false) call.
    ** 009 OM  20131018 Updated to conform the new SessionListener API.
    ** 010 CA  20131220 Refactored to hide the type of the connection (appserver or web service) in a
    **                  ServerHelper implementation. Added support for the web service connection.
    ** 011 CA  20140326 Fixed a NPE bug when cleaning up async requests for a session-managed
    **                  connection.
    ** 012 VIG 20141213 Added SUBTYPE setters stubs.
    ** 013 CA  20150416 Changed CONNECTED: check if the socket connection (if permanent) is still up.
    ** 014 OM  20160323 Moved web services networking access from the P2J server to the client.
    ** 015 CA  20190118 disconnect() must be aware if the FWD session is gone.
    ** 016 CA  20190811 Fixes related to -URL appserver connection option.
    ** 017 CA  20200110 Fixed first/last async resource APIs.
    ** 018 CA  20200427 Added REQUEST-INFO and RESPONSE-INFO support.
    **     CA  20200611 For an AppServer SERVER, the NAME attribute must be the connection ID.
    **     RFB 20200611 The connect method does not need to overwrite the NAME attribute previous set to the
    **                  connection ID. Instead, we move the setting of the name to connectToWebService.
    ** 019 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.
    **     CA  20210204 The requestInfo and responseInfo must be vars.
    **     CA  20220531 Fixed connectToAppServer, to check if the scheme starts with 'appserver'.
    **     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
    ** 020 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 021 GBB 20230620 createThreadName method moved to Utils.
    ** 022 CA  20230620 Do not use 'return' from the 'finally' block, this will consume any thrown exceptions.
    ** 023 GBB 20240101 ConnectHelper renamed to ServerConnectHelper.
    ** 024 CA  20240223 NAME attribute is writeable for SERVER resource.
    ** 025 GBB 20240318 Adding errors on missing sessionModel.
    ** 026 CA  20240910 The ASYNC-REQUEST resources get deleted when the server handle gets deleted, and not on
    **                  disconnect - as the PROCEDURE-COMPLETE event can be processed even if the server got
    **                  disconnected.
    ** 027 GBB 20250403 AppServerManager method renamed to isMultiSession.
    */
    /*
    ** 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.net.*;
    import java.util.*;
    import com.goldencode.p2j.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.net.Session;
    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * A class for implementing APPSERVER methods and attributes.
     */
    public class ServerImpl
    extends HandleChain
    implements Server,
               AsyncRequestCountAttribute,
               FirstLastProcedureAttribute,
               SslServerAttribute,
               SubTypeAttribute,
               ConnectableServer,
               SessionListener,
               AsyncRequestListener
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(ServerImpl.class);

       /** The server helper for this server, set after the {@link #connect} method is called. */
       private ServerHelper helper = null;

       /** The first procedure in this server's proxy procedure list. */
       private ProxyProcedureWrapper firstProcedure = null;

       /** The last procedure in this server's proxy procedure list. */
       private ProxyProcedureWrapper lastProcedure = null;

       /** Flag indicating if the server is valid. */
       private boolean valid = true;

       /** Count the number of active async requests. */
       private long asyncRequestCount = 0;

       /**
        * The currently active timer thread which will cancel the async requests.
        * May be <code>null</code> if no timer is active.
        */
       private Thread cancelReqTimer = null;

       /** An special instance used to perform async request. */
       private AsyncInvoker invoker = null;

       /** 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);

       /** The client types associated with this server handle. Depends on the connect call. */
       private ClientType[] clientTypes;

       /**
        * Default constructor which initializes Server specific settings.
        */
       public ServerImpl()
       {
          requestInfo.assign(ObjectOps.newInstance(OerequestInfo.class));

          requestInfo.ref.setClientContextId(new character(AppServerManager.nextContextId(false)));
       }

       /**
        * Check if the given handle refers a server resource.
        *
        * @param    h
        *
        * @return   <code>true</code> if the handle's resource is a {@link ServerImpl server}.
        */
       public static boolean isServer(handle h)
       {
          return h != null && !h.isUnknown() && (h.get() instanceof ServerImpl);
       }

       /**
        * Returns the client types associated with this server handle. Depends on the connect call.
        *
        * @return   See above.
        */
       public ClientType[] getClientTypes()
       {
          return clientTypes;
       }

       /**
        * Invoke the given request on this server.
        *
        * @param    request
        *           The async request to be executed.
        */
       public void invokeAsync(AsyncRequestImpl request)
       {
          AsyncInvoker invoker = getAsyncInvoker();
          invoker.invoke(request);
       }

       /**
        * Reports if this object is valid for use (has not been deleted).
        *
        * @return   <code>true</code> if we are valid (can be used).
        */
       @Override
       public boolean valid()
       {
          return valid;
       }

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

       /**
        * Returns the number of asynchronous requests.
        *
        * @return   See above.
        */
       @Override
       public synchronized integer getAsyncRequestCount()
       {
          return new integer(asyncRequestCount);
       }

       /**
        * Returns the subtype of the handle.
        * <p>
        * For SERVER handles this represents the type of server to which this is connected.
        * The possible values are "APPSERVER" and "WEBSERVICE".
        *
        * @return   See above.
        */
       @Override
       public character getSubType()
       {
          return !_connected() ? new character() : new character(helper.getSubType());
       }

       /**
        * Sets the the subtype of the object.
        *
        * @param    value
        *           The new value of the subtype object attribute.
        */
       @Override
       public void setSubType(character value)
       {
          handle.readOnlyError(new handle(this), "SUB-TYPE");
       }

       /**
        * Sets the the subtype of the object.
        *
        * @param    value
        *           The new value of the subtype object attribute.
        */
       @Override
       public void setSubType(String value)
       {
          handle.readOnlyError(new handle(this), "SUB-TYPE");
       }

       /**
        * Java implementation for getting the  SSL-SERVER-NAME  attribute  which contains the SSL
        * server name if the connection is made as SSL.
        *
        * @return   The SSL server name, if the connection is made as SSL or unknown
        *           otherwise
        */
       @Override
       public character getSslServerName()
       {
          // TODO Implement the actual logic.
          return new character();
       }

       /**
        * Returns the first entry in the list of persistent procedures or procedure objects.
        *
        * @return   See above.
        */
       @Override
       public handle firstProcedure()
       {
          return new handle(firstProcedure);
       }

       /**
        * Returns the first entry in the list of persistent procedures or procedure objects.
        *
        * @return   See above.
        */
       @Override
       public handle lastProcedure()
       {
          return new handle(lastProcedure);
       }

       /**
        * This returns the id for the AppServer connection to this handle. For Web Services, the
        * returned  value will be an empty string.
        *
        * @return   See above.
        */
       @Override
       public character getClientConnectionId()
       {
          return new character(!_connected() ? "" : helper.getConnectionID());
       }

       /**
        * Returns a handle to the first asynchronous requests in the AppServer or WebService list of
        * asynchronous requests.
        *
        * @return   See above.
        */
       @Override
       public handle getFirstAsyncRequest()
       {
          handle h = HandleChain.firstResource(LegacyResource.ASYNC_REQUEST);
          while (!h.isUnknown())
          {
             handle hs = ((AsyncRequest) h.getResource()).getServerHandle();
             if (hs.getResource() == this)
             {
                return new handle(h);
             }

             h = ((CommonHandleChain) h.getResource()).getNextSibling();
          }

          return new handle(h);
       }

       /**
        * Returns a handle to the last asynchronous requests in the AppServer or WebService list of
        * asynchronous requests.
        *
        * @return   See above.
        */
       @Override
       public handle getLastAsyncRequest()
       {
          handle h = HandleChain.lastResource(LegacyResource.ASYNC_REQUEST);
          while (!h.isUnknown())
          {
             handle hs = ((AsyncRequest) h.getResource()).getServerHandle();
             if (hs.getResource() == this)
             {
                return new handle(h);
             }

             h = ((CommonHandleChain) h.getResource()).getPrevSibling();
          }

          return new handle(h);
       }

       /**
        * Cancel all running or queued requests.  Depending on the server's operating mode, the active
        * requests will treat a cancel command differently:
        * <ul>
        *    <li>Session-managed: STOP is sent to the executing request; the COMPLETE flag will remain
        *        set to TRUE. All the queued requests are purged and their CANCELLED flag is set to
        *        TRUE.</li>
        *    <li>Session-free: STOP is sent to all requests; their CANCELLED flag is set to TRUE.</li>
        * </ul>
        *
        * @return   <code>true</code> if CANCEL-REQUESTS() method returns <code>true</code>,
        *           <code>false</code> otherwise.
        */
       @Override
       public logical cancelRequests()
       {
          if (!_connected())
          {
             return new logical(false);
          }

          AsyncInvoker invoker = getAsyncInvoker();
          return new logical(invoker.cancelRequests());
       }

       /**
        * Calls the {@link #cancelRequests()} method when the specified number of seconds have passed.
        *
        * @param    timeSec
        *           The time in seconds after the {@link #cancelRequests()} method will be called. 4GL
        *           allows only integer or int64 values for this parameter.
        *
        * @return   <code>false</code> if timer cannot be set or {@link #cancelRequests()}'s result
        *           otherwise
        */
       @Override
       public logical cancelRequestsAfter(integer timeSec)
       {
          return cancelRequestsAfter(timeSec.longValue());
       }

       /**
        * Calls the {@link #cancelRequests()} method when the specified number of seconds have passed.
        *
        * @param    timeSec
        *           The time in seconds after the {@link #cancelRequests()} method will be called. 4GL
        *           allows only integer or int64 values for this parameter.
        *
        * @return   <code>false</code> if timer cannot be set or {@link #cancelRequests()}'s result
        *           otherwise
        */
       @Override
       public logical cancelRequestsAfter(int64 timeSec)
       {
          return cancelRequestsAfter(timeSec.longValue());
       }

       /**
        * Calls the {@link #cancelRequests()} method when the specified number of seconds have passed;
        * if another timer exists for this server, it will be terminated and a new timer will start.
        * <p>
        * If <code>timeSec</code> is zero or negative, then the current {@link #cancelReqTimer timer}
        * will be stopped.
        *
        * @param    timeSec
        *           The time in seconds after the {@link #cancelRequests()} method will be called. 4GL
        *           allows only integer or int64 values for this parameter.
        *
        * @return   <code>false</code> if timer cannot be set or {@link #cancelRequests()}'s result
        *           otherwise
        */
       @Override
       public synchronized logical cancelRequestsAfter(long timeSec)
       {
          if (!_connected())
          {
             return new logical(false);
          }

          final long millis = timeSec * 1000;
          if (millis <= 0 && cancelReqTimer != null)
          {
             cancelReqTimer.interrupt();
             return new logical(true);
          }

          Runnable task = new Runnable()
          {
             @Override
             public void run()
             {
                try
                {
                   Thread.sleep(millis);

                   // time has elapsed, send cancel to all currently running or scheduled requests
                   cancelRequests();
                }
                catch (InterruptedException e)
                {
                   // stopped, nothing to do
                }

                synchronized (ServerImpl.this)
                {
                   // clear the timer
                   cancelReqTimer = null;
                }
             }
          };

          // if there is an existing timer, stop it
          if (cancelReqTimer != null)
          {
             cancelReqTimer.interrupt();
          }

          // start the new timer
          cancelReqTimer = new AssociatedThread(task);
          String serverName = helper.getServerName();
          String timerName = String.format("Cancel requests timer [%s]", serverName);
          String name = Utils.createThreadName(timerName);
          cancelReqTimer.setName(name);
          cancelReqTimer.start();

          return new logical(true);
       }

       /**
        * Method declaration  for the CONNECT() procedure supported by web-services and server handles.
        *
        * @param    options
        *           The set of parameters to establish the connection. 4GL allows only
        *           {@link character} values for this parameter.
        *
        * @return   <code>true</code> if connection succeeded, or <code>false</code>
        *           otherwise
        */
       @Override
       public logical connect(String options)
       {
          return connect(new character(options));
       }

       /**
        * Method declaration  for the CONNECT() procedure supported by web-services and server handles.
        *
        * @param    options
        *           The set of parameters to establish the connection. 4GL allows only
        *           character values for this parameter.
        *
        * @return   <code>true</code> if connection succeeded, or <code>false</code>
        *           otherwise
        */
       @Override
       public logical connect(character options)
       {
          return connect(options, new character(), new character(), new character());
       }

       /**
        * Method declaration  for the CONNECT() procedure supported by web-services and server handles.
        *
        * @param    options
        *           The set of parameters to establish the connection. 4GL allows only
        *           character values for this parameter.
        * @param    user
        *           The user to send to the CONNECT procedure on the appserver side.
        *
        * @return   <code>true</code> if connection succeeded, or <code>false</code>
        *           otherwise
        */
       @Override
       public logical connect(character options, character user)
       {
          return connect(options, user, new character(), new character());
       }

       /**
        * Method declaration  for the CONNECT() procedure supported by web-services and server handles.
        *
        * @param    options
        *           The set of parameters to establish the connection. 4GL allows only
        *           character values for this parameter.
        * @param    user
        *           The user to send to the CONNECT procedure on the appserver side.
        * @param    pwd
        *           The password to send to the CONNECT procedure on the appserver side.
        *
        * @return   <code>true</code> if connection succeeded, or <code>false</code>
        *           otherwise
        */
       @Override
       public logical connect(character options, character user, character pwd)
       {
          return connect(options, user, pwd, new character());
       }

       /**
        * Method declaration  for the CONNECT() procedure supported by web-services and server handles.
        *
        * @param    options
        *           The set of parameters to establish the connection. 4GL allows only
        *           character values for this parameter.
        * @param    user
        *           The user to send to the CONNECT procedure on the appserver side.
        * @param    pwd
        *           The password to send to the CONNECT procedure on the appserver side.
        * @param    serverInfo
        *           Other info to send to the CONNECT procedure on the appserver side.
        *
        * @return   <code>true</code> if connection succeeded, or <code>false</code> otherwise
        */
       @Override
       public logical connect(character options, character user, character pwd, character serverInfo)
       {
          if (_connected())
          {
             ErrorManager.displayError(5535, "Server is already connected", false);
             return new logical(false);
          }

          name = null;

          synchronized (this)
          {
             helper = null;
          }

          Map<String, String> parms = parseOptions(options);
          if (parms == null)
          {
             return new logical(false);
          }

          boolean res = false;
          // check the -WSDL parameter, which is required for web services
          boolean webService = parms.containsKey(WebServiceHelper.CONNECT_OPTION_WSDL);
          if (webService)
          {
             // the user, pwd and serverInfo params are ignored
             res = connectToWebService(parms);

             if (!AppServerManager.isMultiSession())
             {
                clientTypes = new ClientType[]{ ClientType._4GLCLIENT, ClientType.WSA };
             }
          }
          else
          {
             res = connectToAppServer(parms, user, pwd, serverInfo);

             if (!AppServerManager.isMultiSession() && parms.containsKey("-URL"))
             {
                clientTypes = new ClientType[]{ ClientType._4GLCLIENT, ClientType.AIA };
             }
          }

          if (!res)
          {
             return new logical(false);
          }

          // register this server with the session
          Session sess = SessionManager.get().getSession();
          sess.addSessionListener(this);
          helper.setServer(this);

          return new logical(true);
       }

       /**
        * Method declaration for the CONNECTED() method, supported by web-services and server handles.
        * This will return <code>true</code> if the web service is connected or <code>false</code>
        * otherwise.
        *
        * @return   <code>true</code> if the server is connected or <code>false</code> otherwise.
        */
       @Override
       public logical connected()
       {
          return new logical(_connected());
       }

       /**
        * Method declaration for the CONNECTED() method, supported by web-services and server handles.
        * This will return <code>true</code> if the web service is connected or <code>false</code>
        * otherwise.
        *
        * @return   <code>true</code> if the server  is connected or <code>false</code> otherwise.
        */
       public synchronized boolean _connected()
       {
          return helper != null && helper.isConnected();
       }

       /**
        * Method declaration for the DISCONNECT() method supported by web-services and server handles.
        * This will attempt to disconnect the resource and returns <code>true</code> if succeeded or
        * <code>false</code> otherwise.
        *
        * @return   <code>true</code> if disconnect succeeded or <code>false</code> otherwise.
        */
       @Override
       public logical disconnect()
       {
          return disconnectImpl(false);
       }

       /**
        * This method is called when the session is ending.  It disconnects the current server, if
        * the session which created it is ending.
        *
        * @param    session
        *           The session that is ending.
        */
       @Override
       public void terminate(Session session)
       {
          if (_connected())
          {
             disconnectImpl(true);
          }
       }

       /**
        * This method is called when the session is starting, just after the context was created.
        * @param   session
        *          The session that is starting.
        */
       @Override
       public void initialize(Session session)
       {
          // no op
       }

       /**
        * Notification that a remote procedure is invoked via this server.
        */
       @Override
       public synchronized void notifyStart()
       {
          asyncRequestCount++;
       }

       /**
        * Notification that a remote procedure invoked via this server is finished.
        */
       @Override
       public synchronized void notifyFinish()
       {
          asyncRequestCount--;
       }

       /**
        * Check if this resource supports the NAME attribute in read-only mode.
        *
        * @return   Always <code>false</code>.
        */
       protected boolean hasNameReadOnly()
       {
          return false;
       }

       /**
        * Check if the server has running async requests. If so, display appropriate error message.
        *
        * @return   <code>true</code> if the server has running async requests.
        */
       synchronized boolean hasAsyncRequests()
       {
          if (asyncRequestCount != 0)
          {
             String msg = "Server %s has outstanding asynchronous requests.  Cannot RUN";
             msg = String.format(msg, helper.getConnectionID());
             ErrorManager.recordOrThrowError(9004, msg, false);

             return true;
          }

          return false;
       }

       /**
        * Get the {@link ServerHelper helper} for this server resource.
        *
        * @return   See above.
        */
       synchronized ServerHelper getServerHelper()
       {
          return helper;
       }

       /**
        * Check if this server is connected to a web-service.
        *
        * @return   See above.
        */
       boolean isWebService()
       {
          return _connected() && helper.isWebService();
       }

       /**
        * Set the first proxy procedure of this server's chain.
        *
        * @param    proc
        *           The procedure instance.
        */
       void setFirstProcedure(ProxyProcedureWrapper proc)
       {
          this.firstProcedure = proc;
       }

       /**
        * Set the last proxy procedure of this server's chain.
        *
        * @param    proc
        *           The procedure instance.
        */
       void setLastProcedure(ProxyProcedureWrapper proc)
       {
          this.lastProcedure = proc;
       }

       /**
        * Delete the resource. If the server is still connected, delete is not possible.
        *
        * @return   <code>true</code> if the resource was deleted.
        */
       @Override
       protected boolean resourceDelete()
       {
          if (_connected())
          {
             final String msg = "SERVER %s is still connected.  Cannot DELETE";
             final String err = String.format(msg, getClientConnectionId().toStringMessage());
             ErrorManager.recordOrThrowError(5457, err);

             return false;
          }

          this.valid = false;

          if (requestInfo._isValid())
          {
             ObjectOps.delete(requestInfo);
          }
          if (responseInfo._isValid())
          {
             ObjectOps.delete(responseInfo);
          }

          // now delete all requests
          handle async = getFirstAsyncRequest();
          while (!async.isUnknown())
          {
             HandleOps.delete(async);
             async = getFirstAsyncRequest();
          }

          return true;
       }

       /**
        * Connect to a web service, using the specified parameters.
        *
        * @param    parms
        *           The connection parameters.
        *
        * @return   <code>true</code> if the connection was possible.
        */
       private boolean connectToWebService(Map<String, String> parms)
       {
          synchronized (this)
          {
             helper = WebServiceHelper.connect(parms);
          }

          boolean connected = _connected();
          if (connected)
          {
             name = helper.getServerName();
          }
          return connected;
       }

       /**
        * Connect to an appserver using the specified parameters.
        *
        * @param    parms
        *           The set of parameters to establish the connection. 4GL allows only character
        *           values for this parameter.
        * @param    user
        *           The user to send to the CONNECT procedure on the appserver side.
        * @param    pwd
        *           The password to send to the CONNECT procedure on the appserver side.
        * @param    serverInfo
        *           Other info to send to the CONNECT procedure on the appserver side.
        *
        * @return   <code>true</code> if the connection was possible.
        */
       private boolean connectToAppServer(Map<String, String> parms,
                                          character           user,
                                          character           pwd,
                                          character           serverInfo)
       {
          String appService = parms.get("-AppService");
          String host = parms.get("-H");
          String serviceName = parms.get("-S");
          boolean directConnect = parms.containsKey("-DirectConnect");

          int port = 0;
          try
          {
             port = Integer.parseInt(serviceName);
          }
          catch (NumberFormatException e)
          {
             // ignore
          }
          String url = parms.get("-URL");
          if (url != null)
          {
             // get them from the URL
             String err = null;
             try
             {
                URI u = new URI(url);
                String protocol = u.getScheme().toLowerCase();
                if (!(protocol.startsWith("appserver") ||
                      protocol.equals("http")          ||
                      protocol.equals("https")))
                {
                   // TODO: is this really needed?
                   // if we are not connecting to an AppServer, than abort
                   err = protocol;
                }
                else
                {
                   host = u.getHost();
                   port = u.getPort();

                   URI uri = new URI(url);
                   serviceName = uri.getPath();
                   directConnect = true;
                }
             }
             catch (URISyntaxException e)
             {
                err = url;
             }

             if (err != null)
             {
                ErrorManager.displayError(String.format("Invalid URL syntax: %s", err));
                ErrorManager.displayError(5468, "Application server connect failure", false);
                return false;
             }
          }

          String sessionModel = ServerConnectHelper.getString(parms, "-sessionModel", null);
          boolean sessionFree = "Session-free".equalsIgnoreCase(sessionModel);

          if (parms.containsKey("-sessionModel") && (sessionModel == null || sessionModel.isEmpty()))
          {
             ErrorManager.recordOrShowWarning(1403, "You have not supplied a parameter for argument " +
                "-sessionModel.");
             ErrorManager.recordOrShowWarning(5509, "Unable to process parameters.");
             return false;
          }
          // TODO: in session-free mode, is possible to establish more than one connection (to allow
          //       async requests). the following parameters are related to session-free mode
          int connectionLifetime = ServerConnectHelper.getInt(parms, "-connectionLifetime", 300);
          int initialConnections = ServerConnectHelper.getInt(parms, "-initialConnections", 1);
          int maxConnections = ServerConnectHelper.getInt(parms, "-maxConnections", 0);
          // the -ns* options are not used by P2J

          try
          {
             synchronized (this)
             {
                Object[] reqInfo = requestInfo.ref.toArray();

                this.helper = AppServerHelper.connectLegacyMode(appService, host, serviceName, port,
                                                                directConnect, sessionModel,
                                                                user, pwd, serverInfo, reqInfo);

                if (this.helper == null)
                {
                   return false;
                }

                // if it was connected, build the response
                ((AppServerHelper) helper).restoreResponse(null, responseInfo);

                // set the NAME attribute to the connection's ID
                name = this.helper.getConnectionID();

                return true;
             }
          }
          catch (NumberedException e)
          {
             ErrorManager.recordOrThrowError(e);
          }
          finally
          {
             if (!_connected())
             {
                LOG.fine("Application server connect failure");
             }
          }

          return false;
       }

       /**
        * Method declaration for the DISCONNECT() method supported by web-services and server handles.
        * This will attempt to disconnect the resource and returns <code>true</code> if succeeded or
        * <code>false</code> otherwise.
        *
        * @param    disconnecting
        *           Flag indicating if the session is being terminated.
        *
        * @return   <code>true</code> if disconnect succeeded or <code>false</code> otherwise.
        */
       private logical disconnectImpl(boolean disconnecting)
       {
          if (!_connected())
          {
             ErrorManager.displayError(5537, "Server is not connected", false);
             return new logical(false);
          }

          // if there is an existing timer, stop it
          if (cancelReqTimer != null)
          {
             cancelReqTimer.interrupt();
          }

          // cancel requests before deleting
          cancelRequests();

          helper.disconnect(disconnecting);

          synchronized (this)
          {
             helper = null;
          }

          if (invoker != null)
          {
             invoker.terminate();
             invoker = null;
          }

          if (!disconnecting)
          {
             Session sess = SessionManager.get().getSession();
             sess.removeSessionListener(this);
          }

          return new logical(true);
       }

       /**
        * Parse and validate the given options; a map is returned with each parameters's value. If a
        * parameter doesn't have a value, the map will contain <code>null</code>.
        *
        * @param    options
        *           The options from which the parameters will be extracted.
        *
        * @return   The parameter-to-value map.
        */
       private Map<String, String> parseOptions(character options)
       {
          if (options.isUnknown())
          {
             // if unknown, it assumes it connects to an AppServer, and not WebService
             return new HashMap<>();
          }

          final Set<String> knownAppServerOptions = new HashSet<>();
          knownAppServerOptions.add("-AppService");
          knownAppServerOptions.add("-H");
          knownAppServerOptions.add("-S");
          knownAppServerOptions.add("-DirectConnect"); // no value
          knownAppServerOptions.add("-ssl"); // no value
          knownAppServerOptions.add("-nosessionreuse"); // no value
          knownAppServerOptions.add("-nohostverify"); // no value
          knownAppServerOptions.add("-pf");
          knownAppServerOptions.add("-AppServerKeepalive");
          knownAppServerOptions.add("-URL");
          knownAppServerOptions.add("-sessionModel");
          knownAppServerOptions.add("-connectionLifetime");
          knownAppServerOptions.add("-initialConnections");
          knownAppServerOptions.add("-maxConnections");
          knownAppServerOptions.add("-nsClientMaxPort");
          knownAppServerOptions.add("-nsClientMinPort");
          knownAppServerOptions.add("-nsClientPicklistExpiration");
          knownAppServerOptions.add("-nsClientPicklistSize");
          knownAppServerOptions.add("-nsClientPortRetry");
          knownAppServerOptions.add("-nsClientDelay");

          final Set<String> noValueOptions = new HashSet<>();
          noValueOptions.add("-DirectConnect"); // no value
          noValueOptions.add("-ssl"); // no value
          noValueOptions.add("-nosessionreuse"); // no value
          noValueOptions.add("-nohostverify"); // no value

          final Set<String> knownWebServiceOptions = WebServiceHelper.getKnownConnectOptions();

          final Map<String, Integer> intParams = new HashMap<>();
          intParams.put("-connectionLifetime", 0);
          intParams.put("-initialConnections", 0);
          intParams.put("-maxConnections", 0);

          String s = options.getValue();

          String[] opts = s.split(" ");
          // determine the target; assume appServer
          Set<String> knownOptions = knownAppServerOptions;
          for (String opt : opts)
          {
             if (WebServiceHelper.CONNECT_OPTION_WSDL.equals(opt))
             {
                knownOptions = knownWebServiceOptions;
                break;
             }
          }

          return ServerConnectHelper.parseOptions(options, knownOptions, noValueOptions, intParams);
       }

       /**
        * Get the {@link #invoker asynchronous invoker} for this server; if {@link #invoker} is null,
        * it will create a {@link SessionManagedInvoker} if the server is in the Session-managed
        * operating mode or a {@link SessionFreeInvoker} if the server is in the Session-free
        * operating mode.
        *
        * @return   See above.
        */
       private AsyncInvoker getAsyncInvoker()
       {
          if (invoker == null)
          {
             invoker = (helper.isSessionFree() ? new SessionFreeInvoker()
                                               : new SessionManagedInvoker());

             // start in a different thread, but same context
             invoker.start();
          }

          return invoker;
       }

       /**
        * Provides a mechanism of performing asynchronous requests, by hiding the operating state of
        * this server (State-free or State-managed).
        */
       private abstract class AsyncInvoker
       {
          /** Flag indicating the server is still available for */
          private boolean terminated;

          /**
           * Invoke the async request, based on the operating mode.
           *
           * @param    request
           *           The request to be invoked.
           */
          public abstract void invoke(AsyncRequestImpl request);

          /**
           * Start this async invoker.
           */
          public abstract void start();

          /**
           * Cancel all the active requests.
           *
           * @return    <code>true</code> if cancellation was possible.
           */
          public abstract boolean cancelRequests();

          /**
           * Terminate this async invoker by setting the {@link #terminated} flag to <code>true</code>.
           */
          public synchronized void terminate()
          {
             this.terminated = true;
          }

          /**
           * Check if this async invoker is terminated.
           *
           * @return   The state of the {@link #terminated} flag.
           */
          public synchronized boolean isTerminated()
          {
             return terminated;
          }
       }

       /**
        * Asynchronous invoker implementation for Session-managed operating mode.  In this mode, a
        * separate thread is started which listens for async requests added to the queue, and executes
        * them in a FIFO mode.
        */
       private class SessionManagedInvoker
       extends AsyncInvoker
       implements Runnable
       {
          /** Queue of async requests pending execution. */
          private ArrayDeque<AsyncRequestImpl> queue = new ArrayDeque<AsyncRequestImpl>();

          /** The current async request being executed. */
          private AsyncRequestImpl request = null;

          /**
           * In Session-managed operating mode, invoking an async request means adding it to the
           * {@link #queue async queue}; from there, a specialized thread will pick it up and execute
           * it.
           *
           * @param    request
           *           The async request to be executed.
           */
          @Override
          public void invoke(AsyncRequestImpl request)
          {
             synchronized (queue)
             {
                queue.addLast(request);
                queue.notify();
             }
          }

          /**
           * In Session-managed operating mode, a dedicated thread is started which listens for
           * async requests added to the {@link #queue}.
           */
          @Override
          public void start()
          {
             Thread t = new AssociatedThread(this);

             // register this thread as async
             SessionManager.get().registerAsyncThread(t);

             String serverName = helper.getServerName();
             String asyncName = String.format("Session-managed invoker [%s]", serverName);
             String name = Utils.createThreadName(asyncName);

             t.setName(name);
             t.start();
          }

          /**
           * Listens for incoming async requests and executes them in FIFO order.
           */
          @Override
          public void run()
          {
             try
             {
                // listen for requests while the server is connected
                while (!isTerminated() && _connected())
                {
                   synchronized (queue)
                   {
                      if (queue.isEmpty())
                      {
                         try
                         {
                            // as we can't sync on this object, make this wait a max amount of time
                            queue.wait(5000);
                         }
                         catch (InterruptedException e)
                         {
                            // restart the loop
                            continue;
                         }
                      }

                      if (queue.isEmpty())
                      {
                         // if we are still empty, restart the loop, maybe it was terminated
                         continue;
                      }

                      // get all requests and process them separately, in a FIFO order
                      request = queue.removeFirst();
                   }

                   // 'false' is used because we execute the request on this thread.
                   request.execute(false);
                }
             }
             finally
             {
                // de-register this thread from the async list
                SessionManager.get().deregisterAsyncThread(Thread.currentThread());
             }
          }

          /**
           * Cancel all queued or running requests.
           */
          public boolean cancelRequests()
          {
             // process the queue
             synchronized (queue)
             {
                if (request != null && !request._isComplete())
                {
                   // send STOP to current request and wait to complete
                   request.stop();
                }

                // notify all requests that they are cancelled
                for (AsyncRequestImpl req : queue)
                {
                   req.cancelled();
                }

                // clear the queue
                queue.clear();
             }

             return true;
          }
       }

       /**
        * Asynchronous invoker implementation for Session-free operating mode.  In this mode, each
        * async request is executed in its separate thread.
        */
       private class SessionFreeInvoker
       extends AsyncInvoker
       {
          /** A set of running requests. */
          private Set<AsyncRequestImpl> requests = new HashSet<AsyncRequestImpl>();

          /**
           * Invoking an async request in State-free mode needs to request to be executed in its
           * separated thread. For this, all running requests are registered in the {@link #requests}
           * set, to be able to cancel them.
           *
           * @param    request
           *           The request to be executed.
           */
          @Override
          public void invoke(final AsyncRequestImpl request)
          {
             synchronized (requests)
             {
                requests.add(request);
             }

             // each request has its own thread
             Thread t = new AssociatedThread(new Runnable()
             {
                public void run()
                {
                   try
                   {
                      // 'false' is used because we execute the request on this thread.
                      request.execute(false);
                   }
                   finally
                   {
                      synchronized (requests)
                      {
                         requests.remove(request);
                      }

                      // de-register this thread from the async list
                      SessionManager.get().deregisterAsyncThread(Thread.currentThread());
                   }
                }
             });

             // register this thread as async
             SessionManager.get().registerAsyncThread(t);

             String pname = request.getProcedureName().toStringMessage();
             String serverName = helper.getServerName();

             String asyncName = String.format("Session-free invoker [%s:%s]", serverName, pname);
             String name = Utils.createThreadName(asyncName);

             t.setName(name);
             t.start();
          }

          /**
           * This is a no-op, as the State-free invoker doesn't need a dedicated thread.
           */
          @Override
          public void start()
          {
             // no-op
          }

          /**
           * Cancel all running requests by sending the STOP conditions to them.
           *
           * @return   <code>true</code> if cancel was possible.
           */
          @Override
          public boolean cancelRequests()
          {
             synchronized (requests)
             {
                for (AsyncRequestImpl request : requests)
                {
                   // send STOP to this request
                   if (!request._isComplete())
                   {
                      // send stop to current request and wait it to complete
                      request.stop();

                      // mark as cancelled too
                      request.cancelled();
                   }
                }

                requests.clear();
             }

             return true;
          }
       }
    }