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