LegacySocketLibrary.java

/*
** Module   : ABLSocketLibrary.java
** Abstract : Implementation of the builtin class.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20190526 First version, stubs taken by converting the skeleton using FWD.
** 002 IAS 20190527 Added multipart support, got rid of "abl" in names.
** 003 CA  20191024 Added method support levels and updated the class support level.
** 004 IAS 20191108 Added HTTP Redirect support.
** 005 MP  20200602 Add STUB constructors, properties and methods requested by calls made
**                  by LegacySocketLibraryBuilder class.
** 006 ME  20210128 Add query parameters and fragment when executing request.
**         20210130 Fix call to 'next' iterator method.
**         20210210 Add support for proxy, use version info from request.
**     CA  20210219 OO input parameter definitions for functions/procedures must not register with  ObjectOps 
**                  - TypeFactory.initInput will take care of this.
** 007 CA  20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy signature.
** 008 ME  20210426 Removed default ctor, add destructor to clean-up client socket and connect params.
** 009 ME  20210524 Added request timeout, improve error handling (4GL style).
**     CA  20220120 Do not used TypeFactory.object when the OO reference must not be tracked or registered.
**                  Do not use TypeFactory.object for internal usages, use ObjectVar if the reference must 
**                  be tracked.
**                  All TypeFactory.object variable definitions must be done outside of the top-level block.
**     ME  20210929 Check for unknown port number.
**     ME  20220918 Fix check for port number, should not be negative not unknown.
**     CA  20220923 Variable definitions (including associated with parameters) must be done always outside of 
**                  the BlockManager API
**     CA  20221026 Do not pass null to 'readTerminatedHandler' call (all arguments must handle unknown as
**                  BDT instances).
** 010 CA  20230313 In 'execute', catch and throw any ConditionException (as there is a 'catch Exception' 
**                  block for anything else).
** 011 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 012 CA  20230704 Added a name for the Timer thread and made it daemon.
** 013 CA  20230817 Removed ObjectOps.register, as this registration is handled by runtime.
** 014 SBI 20231002 Changed _execute(...) to fix url encoding for query.
** 015 CA  20231113 The 'execute' method must be annotated with LegacySignature Type.Execute, and also can be
**                  dropped if is a no-op.
** 016 AL2 20240930 Delete temporary ByteBuckets; they are still in memory due to positive reference count.
** 017 CA  20250515 'execute' must log any exception being thrown; also, Content-Length and Transfer-Encoding
**                  must not be written to the request, as Apache HttpClient will write them always by
**                  itself, when needed - if they appear twice, the request fails.
** 018 AL2 20250530 Added extra logging for failing requests without known cause.
*/

/*
** 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.oo.net.http.lib.sockets;

import static com.goldencode.p2j.util.BlockManager.externalProcedure;
import static com.goldencode.p2j.util.BlockManager.function;
import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.BlockManager.internalProcedure;
import static com.goldencode.p2j.util.BlockManager.onBlockLevel;
import static com.goldencode.p2j.util.BlockManager.returnNormal;
import static com.goldencode.p2j.util.BlockManager.undoThrow;

import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.security.*;
import java.security.cert.*;
import java.util.*;
import java.util.logging.*;
import java.util.stream.Collectors;

import javax.net.ssl.*;

import com.goldencode.p2j.util.logging.*;
import org.apache.http.*;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolException;
import org.apache.http.client.*;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.*;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.*;
import org.apache.http.config.*;
import org.apache.http.conn.*;
import org.apache.http.conn.socket.*;
import org.apache.http.conn.ssl.*;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.*;
import org.apache.http.protocol.*;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.*;

import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.oo.core.*;
import com.goldencode.p2j.oo.core.collections.IStringStringMap;
import com.goldencode.p2j.oo.core.collections.Iiterator;
import com.goldencode.p2j.oo.json.objectmodel.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.oo.logging.ISupportLogging;
import com.goldencode.p2j.oo.logging.VoidLogger;
import com.goldencode.p2j.oo.net.*;
import com.goldencode.p2j.oo.net.http.*;
import com.goldencode.p2j.oo.net.http.filter.payload.*;
import com.goldencode.p2j.oo.net.http.filter.writer.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.BlockManager.Action;
import com.goldencode.p2j.util.BlockManager.Condition;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.InternalEntry.*;
import com.goldencode.p2j.xml.*;

/**
 * 
 * Performs actual HTTP request 
 *
 */
@LegacyResource(resource = "OpenEdge.Net.HTTP.Lib.ABLSockets.ABLSocketLibrary")
@LegacyResourceSupport(supportLvl = CVT_LVL_PARTIAL|RT_LVL_PARTIAL)
public class LegacySocketLibrary
extends com.goldencode.p2j.oo.lang.BaseObject
implements IHttpClientLibrary, ISupportLogging
{
   @LegacySignature(type = Type.PROPERTY, name = "Logger")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   private object<? extends com.goldencode.p2j.oo.logging.IlogWriter> logger = TypeFactory.object(com.goldencode.p2j.oo.logging.IlogWriter.class);

   @LegacySignature(type = Type.PROPERTY, name = "LibraryName")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   private character libraryName = TypeFactory.character();

   @LegacySignature(type = Type.PROPERTY, name = "LibraryVersion")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   private character libraryVersion = TypeFactory.character();

   @LegacySignature(type = Type.PROPERTY, name = "Options")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   private object<? extends com.goldencode.p2j.oo.net.http.ClientOptions> options = TypeFactory.object(com.goldencode.p2j.oo.net.http.ClientOptions.class);
   
   private object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> moSocketLib = TypeFactory.object(com.goldencode.p2j.oo.net.serverconnection.ClientSocket.class);
   
   private object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters> moSocketConnectionParam = 
            TypeFactory.object(com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters.class);
   
   /** The root node for configuration */
   private static final String CONFIG_ROOT = "/security/http-client";

   /** Buffer size */
   private static final int BUFSIZE = 4096;

   /** logger */
   private static final CentralLogger LOG = CentralLogger.get(LegacySocketLibrary.class.getName());

   /** SSL Context */
   private static final ContextLocal<SSLContext> SSL_CONTEXT = new ContextLocal<SSLContext>()
   {
      /**
       * Create initial value
       */
      @Override
      protected SSLContext initialValue()
      {
         try
         {
            return getSSLContext();
         } 
         catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException
               | CertificateException | IOException e)
         {
            throw new RuntimeException("Failed to create SSLContext", e);
         }
      }
   };

   /** HttpClientConnectionManager */
   private static final ContextLocal<HttpClientConnectionManager> CONNECTION_NANAGER = 
         new ContextLocal<HttpClientConnectionManager>()
   {
      /**
       * Create initial value
       */
      @Override
      protected HttpClientConnectionManager initialValue()
      {
         return getHttpClientConnectionManager(SSL_CONTEXT.get());
      }
   };

   /** 
    * Create SSLContext
    * 
    * @return SSLContext
    * 
    * @throws KeyManagementException
    * @throws NoSuchAlgorithmException
    * @throws KeyStoreException
    * @throws CertificateException
    * @throws IOException
    */
   private static SSLContext getSSLContext() 
   throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException, 
          CertificateException, IOException {
      DirectoryService ds = DirectoryService.getInstance();
      if (!ds.bind())
      {
         throw new RuntimeException("Directory bind failed");
      }
      Boolean acceptSelfSigned = ds.getNodeBoolean(CONFIG_ROOT + "/accept-self-signed", "value");
      Boolean acceptAll = ds.getNodeBoolean(CONFIG_ROOT + "/accept-all", "value");
      String ksFile = ds.getNodeString(CONFIG_ROOT + "/keystore/file", "value");
      String ksPwd = ds.getNodeString(CONFIG_ROOT + "/keystore/password", "value");
      TrustStrategy trustStrategy;
      if (!Boolean.FALSE.equals(acceptAll))
      {
         LOG.log(Level.INFO, "Http client accepts all certificates");
         trustStrategy =  new TrustStrategy() 
         {
            @Override
            public boolean isTrusted(X509Certificate[] chain, String authType) 
            throws CertificateException
            {
               return true;
            }
         };
      }
      else if (!Boolean.FALSE.equals(acceptSelfSigned)) 
      {
         LOG.log(Level.INFO, "Http client accepts self-signed certificates");
         trustStrategy = TrustSelfSignedStrategy.INSTANCE; 
         
      }
      else 
      {
         trustStrategy =  new TrustStrategy() 
         {
            @Override
            public boolean isTrusted(X509Certificate[] chain, String authType) 
            throws CertificateException
            {
               return false;
            }
         };
      }
      if (ksFile != null)
      {
         File ks = new File(ksFile);
         LOG.log(Level.INFO, "Using trusted keystrore: " + ks.getAbsolutePath());
         return new SSLContextBuilder().loadTrustMaterial(ks, 
               ksPwd != null ? ksPwd.toCharArray() : null , trustStrategy).build();
      }
      return new SSLContextBuilder().loadTrustMaterial(trustStrategy).build(); 
   }
   
   /**
    * Create HttpClientConnectionManager
    * @param sslContext SSLContet
    * 
    * @return HttpClientConnectionManager
    */
   private static HttpClientConnectionManager getHttpClientConnectionManager(SSLContext sslContext)
   {
      HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
      SSLConnectionSocketFactory sslSocketFactory = 
            new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
      
      org.apache.http.config.Registry<ConnectionSocketFactory> socketFactoryRegistry =
            RegistryBuilder.<ConnectionSocketFactory>create().
            register("http", PlainConnectionSocketFactory.getSocketFactory()).
            register("https", sslSocketFactory).
            build();
      
      return new PoolingHttpClientConnectionManager(socketFactoryRegistry);
   }
   
   /**
    * Get Library name.
    */
   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "LibraryName")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   @Override
   public character getLibraryName()
   {
      return function(LegacySocketLibrary.class, this, "LibraryName", character.class, new Block((Body) () -> 
      {
         returnNormal(libraryName);
      }));
   }

   /**
    * Get Library version
    */
   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "LibraryVersion")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   @Override
   public character getLibraryVersion()
   {
      return function(LegacySocketLibrary.class, this, "LibraryVersion", character.class, new Block((Body) () -> 
      {
         returnNormal(libraryVersion);
      }));
   }

   /**
    * Get Client options
    */
   @LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Net.HTTP.ClientOptions", type = Type.GETTER, name = "Options")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   @Override
   public object<? extends com.goldencode.p2j.oo.net.http.ClientOptions> getOptions()
   {
      return function(LegacySocketLibrary.class, this, "Options", object.class, new Block((Body) () -> 
      {
         returnNormal(options);
      }));
   }
   
   /**
    * Get Client logger
    */
   @LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Logging.ILogWriter", type = Type.GETTER, name = "Logger")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_PARTIAL)
   @Override
   public object<? extends com.goldencode.p2j.oo.logging.IlogWriter> getLogger()
   {
      return function(LegacySocketLibrary.class, this, "Logger", object.class, new Block((Body) () -> 
      {
         // TODO: use logger builder when available instead of void logger
         if (!logger._isValid())
            logger.assign(ObjectOps.newInstance(VoidLogger.class));
         returnNormal(logger);
      }));
   }
   
   /**
    * Set Client logger
    */
   @Override
   @LegacySignature(type = Type.SETTER, name = "Logger", parameters = 
   {
      @LegacyParameter(qualified = "OpenEdge.Logging.ILogWriter", name = "var", type = "OBJECT", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void setLogger(final object<? extends com.goldencode.p2j.oo.logging.IlogWriter> _var)
   {
      object<? extends com.goldencode.p2j.oo.logging.IlogWriter> var = TypeFactory.initInput(_var);
      
      internalProcedure(LegacySocketLibrary.class, this, "Logger", new Block((Body) () -> 
      {
         logger.assign(var);
      }));
   }

   /**
    * Execute HTTP request
    * 
    * @param _poRequest HTTP request
    * @param _poResponse HTTP response
    */
   @LegacySignature(type = Type.METHOD, name = "Execute", parameters = 
   {
      @LegacyParameter(name = "poRequest", type = "OBJECT", 
            qualified = IHTTPREQUEST, mode = "INPUT"),
      @LegacyParameter(name = "poResponse", type = "OBJECT", 
            qualified = IHTTPRESPONSE, mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   @Override
   public void execute(object<? extends IhttpRequest> _poRequest, object<? extends IhttpResponse> _poResponse)
   {
      object<? extends IhttpRequest> poRequest = TypeFactory.initInput(_poRequest);
      object<? extends IhttpResponse> poResponse = TypeFactory.initInput(_poResponse);
      object<? extends Uri> proxyUri = TypeFactory.object(Uri.class);
      object<? extends MessageWriter> entityWriter = TypeFactory.object(MessageWriter.class);
      object[][] headers = { TypeFactory.objectExtent(HttpHeader.class, false) };
      
      internalProcedure(this, "execute", new Block((Body) () -> 
      {

         Assert.notNull(poRequest, new character("HTTP request"));
         Assert.notNull(poResponse, new character("HTTP response"));

         integer port = poRequest.ref().getUri().ref().getPort();
         
         // too high port number checked by client socket probably, comes before host check
         if (!port.isUnknown() && port.intValue() > 65535)
         {
            undoThrow(AppError.newInstance(String.format("Port is too large: %d", port.intValue()), 0));
         }
         
         character host = poRequest.ref().getUri().ref().getHost();

         // spaces in host name is breaking the connection params (-H xxxx yyyy)
         if (TextOps.numEntries(host, " ").intValue() > 1)
         {
            ErrorManager.recordOrThrowError(new int[] { 301, 5509, 5510 },
                     new String[] {
                              String.format("** Could not recognize argument: %s",
                                       TextOps.entry(2, host, " ").toStringMessage()),
                              "Unable to process parameters", "Invalid parameter string" },
                     false);
         }

         // negative port is breaking the connection params (-S -xxxx)
         if (!port.isUnknown() && port.intValue() < 0)
         {
            ErrorManager.recordOrThrowError(new int[] { 1403, 5509, 5510 },
                     new String[] { "You have not supplied a parameter for argument -S",
                              "Unable to process parameters", "Invalid parameter string" },
                     false);
         }

         try
         {
            _execute(poRequest, poResponse, proxyUri, entityWriter, headers);
         }
         catch (UnknownHostException e)
         {
            undoThrow(SysError.newInstance(String.format("Unknown hostname %s",
                     poRequest.ref().getUri().ref().getHost()), 5482, false, true));
         }
         catch (HttpHostConnectException e)
         {
            undoThrow(SysError.newInstance(
                     String.format("Connection failure for host %s port %d transport TCP",
                              e.getHost().getHostName(), e.getHost().getPort()),
                     9407, false, true));
         }
         catch (HttpException exc)
         {
            LOG.log(Level.SEVERE, "HttpException while executing HTTP request", exc);

            // request terminated
            readTerminatedHandler(new object(), new object());
         }
         catch (IOException e)
         {
            LOG.log(Level.WARNING, "IOException while executing HTTP request", e);
            // request terminated
            readTerminatedHandler(new object(), new object());
         }
         catch (ConditionException cex)
         {
            throw cex;
         }
         catch (Exception e)
         {
            String msg = String.format("Unknown exception: %s", e.getMessage());
            if (LOG.isLoggable(Level.SEVERE))
            {
               LOG.severe(msg, e);
            }
            undoThrow(AppError.newInstance(msg, 0));
         }

      }));
   }

   /**
    * Destructor
    */
   @LegacySignature(type = Type.DESTRUCTOR)
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void __net_http_lib_sockets_LegacySocketLibrary_destructor__()
   {
      internalProcedure(this, "__net_http_lib_ablsockets_ABLSocketLibrary_destructor__", new Block((Body) () -> 
      {
         moSocketLib.setUnknown();
         moSocketConnectionParam.setUnknown();
      }));
   }
   
   @LegacySignature(type = Type.CONSTRUCTOR, parameters = 
   {
      @LegacyParameter(name = "pcLibraryName", type = "CHARACTER", mode = "INPUT"),
      @LegacyParameter(name = "pcLibraryVersion", type = "CHARACTER", mode = "INPUT"),
      @LegacyParameter(name = "poSocket", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
      @LegacyParameter(name = "poSocketConnectionParams", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocketconnectionparameters", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void __net_http_lib_sockets_LegacySocketLibrary_constructor__(
            final character _pcLibraryName, 
            final character _pcLibraryVersion, 
            final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSocket, 
            final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters> _poSocketConnectionParams)
   {
      __net_http_lib_sockets_LegacySocketLibrary_constructor__(_pcLibraryName, _pcLibraryVersion, _poSocket, _poSocketConnectionParams, 
               ObjectOps.newInstance(com.goldencode.p2j.oo.net.http.ClientOptions.class));
   }

   @LegacySignature(type = Type.CONSTRUCTOR, parameters = 
   {
      @LegacyParameter(name = "pcLibraryName", type = "CHARACTER", mode = "INPUT"),
      @LegacyParameter(name = "pcLibraryVersion", type = "CHARACTER", mode = "INPUT"),
      @LegacyParameter(name = "poSocket", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
      @LegacyParameter(name = "poSocketConnectionParams", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocketconnectionparameters", mode = "INPUT"),
      @LegacyParameter(name = "poOptions", type = "OBJECT", qualified = "openedge.net.http.clientoptions", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void __net_http_lib_sockets_LegacySocketLibrary_constructor__(
            final character _pcLibraryName, 
            final character _pcLibraryVersion, 
            final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSocket, 
            final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters> _poSocketConnectionParams, 
            final object<? extends com.goldencode.p2j.oo.net.http.ClientOptions> _poOptions)
   {
      character pcLibraryName = TypeFactory.initInput(_pcLibraryName);
      character pcLibraryVersion = TypeFactory.initInput(_pcLibraryVersion);
      object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> poSocket = TypeFactory.initInput(_poSocket);
      object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters> poSocketConnectionParams = TypeFactory.initInput(_poSocketConnectionParams);
      object<? extends com.goldencode.p2j.oo.net.http.ClientOptions> poOptions = TypeFactory.initInput(_poOptions);
      
      internalProcedure(LegacySocketLibrary.class, this, "__net_http_lib_ablsockets_ABLSocketLibrary_constructor__", new Block((Body) () -> 
      {
         __lang_BaseObject_constructor__();
         Assert.notNull(poSocket, new character("Client socket"));
         Assert.notNull(poSocketConnectionParams, new character("Default connection parameters"));
         Assert.notNullOrEmpty(pcLibraryName, new character("Library Name"));
         Assert.notNullOrEmpty(pcLibraryVersion, new character("Library Version"));
         Assert.notNull(poOptions, new character("Client options"));
         
         libraryName.assign(pcLibraryName);
         libraryVersion.assign(pcLibraryVersion);
         options.assign(poOptions);
         moSocketLib.assign(poSocket);
         moSocketConnectionParam.assign(poSocketConnectionParams);
         
      }));
   }

   /**
    * Execute method
    */
   @LegacySignature(type = Type.EXECUTE)
   public void __net_http_lib_sockets_LegacySocketLibrary_execute__()
   {
      onBlockLevel(Condition.ERROR, Action.THROW);
   }
   
   @LegacySignature(type = Type.METHOD, name = "CreateTunnel", parameters = 
   {
      @LegacyParameter(name = "pProxyReq", type = "OBJECT", qualified = "openedge.net.http.isupportproxy", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
   protected void createTunnel(final object<? extends com.goldencode.p2j.oo.net.http.ISupportProxy> _pProxyReq)
   {
      object<? extends com.goldencode.p2j.oo.net.http.ISupportProxy> pProxyReq = TypeFactory.initInput(_pProxyReq);
      
      internalProcedure(LegacySocketLibrary.class, this, "CreateTunnel", new Block((Body) () -> 
      {
         UnimplementedFeature.missing("ABLSocketLibrary:CreateTunnel METHOD");
      }));
   }
   
   @LegacySignature(type = Type.METHOD, name = "DataReceivedHandler", parameters = 
   {
      @LegacyParameter(name = "poSender", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
      @LegacyParameter(name = "poEventArgs", type = "OBJECT", qualified = "openedge.net.serverconnection.socketreadeventargs", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
   public void dataReceivedHandler(final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSender, final object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> _poEventArgs)
   {
      object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> poSender = TypeFactory.initInput(_poSender);
      object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> poEventArgs = TypeFactory.initInput(_poEventArgs);
      
      internalProcedure(LegacySocketLibrary.class, this, "DataReceivedHandler", new Block((Body) () -> 
      {
         UnimplementedFeature.missing("ABLSocketLibrary:DataReceivedHandler METHOD");
      }));
   }
   
   @LegacySignature(type = Type.METHOD, name = "MakeSyncRequest", parameters = 
   {
      @LegacyParameter(name = "pServer", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
      @LegacyParameter(name = "pRequest", type = "OBJECT", qualified = "openedge.net.http.ihttprequest", mode = "INPUT"),
      @LegacyParameter(name = "pResponse", type = "OBJECT", qualified = "openedge.net.http.ihttpresponse", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
   protected void makeSyncRequest(final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _pServer, final object<? extends com.goldencode.p2j.oo.net.http.IhttpRequest> _pRequest, final object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> _pResponse)
   {
      object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> pServer = TypeFactory.initInput(_pServer);
      object<? extends com.goldencode.p2j.oo.net.http.IhttpRequest> pRequest = TypeFactory.initInput(_pRequest);
      object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> pResponse = TypeFactory.initInput(_pResponse);
      
      internalProcedure(LegacySocketLibrary.class, this, "MakeSyncRequest", new Block((Body) () -> 
      {
         UnimplementedFeature.missing("ABLSocketLibrary:MakeSyncRequest METHOD");
      }));
   }
   
   @LegacySignature(type = Type.METHOD, name = "ReadTerminatedHandler", parameters = 
   {
      @LegacyParameter(name = "poSender", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
      @LegacyParameter(name = "poEventArgs", type = "OBJECT", qualified = "openedge.net.serverconnection.socketreadeventargs", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void readTerminatedHandler(final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSender, final object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> _poEventArgs)
   {
      object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> poSender = TypeFactory.initInput(_poSender);
      object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> poEventArgs = TypeFactory.initInput(_poEventArgs);
      
      internalProcedure(LegacySocketLibrary.class, this, "ReadTerminatedHandler", new Block((Body) () -> 
      {
         undoThrow(AppError.newInstance(TextOps.substitute("Read terminated for &1", 
                  moSocketConnectionParam.ref().getUri().ref().toLegacyString()), new integer(105)));
      }));
   }
   
   @LegacySignature(type = Type.METHOD, name = "ReadTimeoutHandler", parameters = 
   {
      @LegacyParameter(name = "poSender", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
      @LegacyParameter(name = "poEventArgs", type = "OBJECT", qualified = "openedge.net.serverconnection.socketreadeventargs", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void readTimeoutHandler(final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSender, final object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> _poEventArgs)
   {
      object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> poSender = TypeFactory.initInput(_poSender);
      object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> poEventArgs = TypeFactory.initInput(_poEventArgs);
      
      internalProcedure(LegacySocketLibrary.class, this, "ReadTimeoutHandler", new Block((Body) () -> 
      {
         undoThrow(ObjectOps.newInstance(HttpRequestError.class, "III", 
                  new character(StatusCodeEnum.requestTimeOut),
                  new character("read"),
                  moSocketConnectionParam.ref().getUri().ref().toLegacyString()));
      }));
   }
   
   /**
    * Execute HTTP request and polulate HTTP response
    * 
    * @param req
    *        request
    * @param rsp
    *        response
    * @throws Exception 
    */
   private void _execute(object<? extends IhttpRequest> req, 
                         object<? extends IhttpResponse> rsp, 
                         object<? extends Uri> proxyUri,
                         object<? extends MessageWriter> entityWriter,
                         object[][] headers)
   throws Exception
   {
      Uri uri = req.ref().getUri().ref();
      String method = req.ref().getMethod().getValue();
      String baseUri = uri.getBaseUri().getValue();
      String path = uri.getPath().getValue();
      String scheme = uri.getScheme().getValue();
      String host = uri.getHost().getValue();
      
      //URI java class will assign -1 to port if is not defined
      //we change it to unknown to match our logic
      int port = -1;
      if (!uri.getPort().isUnknown())
      {
         port = uri.getPort().intValue();
      }
      
      java.net.URI _uri;
      URIBuilder builder = new URIBuilder().setScheme(scheme).setHost(host).setPort(port)
               .setPath(path);

      String encoding = I18nOps.getJavaCharset(req.ref().getCharacterEncoding());

      if (encoding != null && Charset.isSupported(encoding))
         builder.setCharset(Charset.forName(encoding));

      object<? extends IStringStringMap> qryMap = uri.getQueryMap_1();

      if (qryMap._isValid() && qryMap.ref().getSize().intValue() > 0)
      {
         object<? extends Iiterator> iterator = qryMap.ref().getKeySet().ref().iterator();
         while (iterator.ref().hasNext().booleanValue())
         {
            object<? extends _BaseObject_> key = iterator.ref().next_();
            object<? extends _BaseObject_> value = qryMap.ref().get(key);

            builder.addParameter(key.ref().toLegacyString().getValue(),
                     Uri.decode(value.ref().toLegacyString(), new character(encoding)).getValue());
         }
      }

      String fragment = uri.getFragment().getValue();

      if (!TextOps.isEmpty(fragment))
         builder.setFragment(fragment);

      _uri = builder.build();

      String uriAscii = _uri.toASCIIString();
      uriAscii = uriAscii.replaceAll("%3A", ":");//4GL does not encode colons in query string.
      
      org.apache.http.client.methods.RequestBuilder requestBuilder = org.apache.http.client.methods.RequestBuilder
               .create(method).setUri(uriAscii);

      if (req.ref().hasHeader(new character("Content-Type")).booleanValue())
      {
         HttpEntity httpEntity = createEntity(req);
         requestBuilder.setEntity(httpEntity);
      }
      setHeaders(req, requestBuilder, headers);
      
      String[] ver = req.ref().getVersion().getValue().split("[/.]+");
      int majVer = ver.length > 1 ? Integer.parseInt(ver[1]) : 1;
      int minVer = ver.length > 2 ? Integer.parseInt(ver[2]) : 1;
      
      ProtocolVersion pv = new ProtocolVersion(ver[0], majVer, minVer);
      requestBuilder.setVersion(pv);

      HttpUriRequest request = requestBuilder.build();
      
      if (ObjectOps.typeOf(req, ISupportProxy.class).booleanValue())
      {
         proxyUri.assign(ObjectOps.cast(req, ISupportProxy.class).ref().getProxyURI());
      }
      
      org.apache.http.client.HttpClient client = createHttpClient(proxyUri);

      // request hard timeout
      double reqTimeout = options.ref().getRequestTimeout().doubleValue();
      
      if (reqTimeout > 0)
      {
         Timer timer = new Timer("Request timeout timer", true);
         TimerTask task = new TimerTask()
         {
            @Override
            public void run()
            {
               if (request != null)
               {
                  request.abort();
               }
               timer.cancel();
            }
         };
         timer.schedule(task, (long) (reqTimeout * 1000));
      }
      
      HttpResponse response = client.execute(request);

      StatusLine sline = response.getStatusLine();
      rsp.ref().setStatusCode(new integer(sline.getStatusCode()));
      rsp.ref().setStatusReason(new character(sline.getReasonPhrase()));
      rsp.ref().setVersion(new character(
    		  sline.getProtocolVersion().toString()));

      extractCookies(response, rsp.ref());
      
      extractHeaders(response, rsp.ref());

      if (!"HEAD".equalsIgnoreCase(method))
      {
         try
         {
            extractEntity(response, rsp, entityWriter);
         } 
         catch (IOException e)
         {
            LOG.log(Level.WARNING, "HTTP response processing failed", e);
            rsp.ref().setStatusCode(new integer(500));
            rsp.ref().setStatusReason(new character("HTTP response processing failed"));
         }
      }
   }

   /**
    * Create HTTP client
    * 
    * @return Apache HTTPClient
    */
   private org.apache.http.client.HttpClient createHttpClient(object<? extends Uri> proxyUri)
   {
      
      SSLContext sslContext = SSL_CONTEXT.get();
      HttpClientConnectionManager connectionManager = CONNECTION_NANAGER.get();

      HttpClientBuilder builder = HttpClientBuilder.create().
            setConnectionManager(connectionManager).
            // Redirect should be handled by the RequstStatusFilter for all methods
            setRedirectStrategy(new RedirectStrategy() 
            {
               
               @Override
               public boolean isRedirected(HttpRequest arg0, HttpResponse arg1, HttpContext arg2)
                     throws ProtocolException
               {
                  return false;
               }
               
               @Override
               public HttpUriRequest getRedirect(HttpRequest arg0, HttpResponse arg1, HttpContext arg2)
                     throws ProtocolException
               {
                  return null;
               }
            }).
            setSSLContext(sslContext).
            setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build());
      
      if (proxyUri._isValid())
      {
         HttpHost proxy = new HttpHost(proxyUri.ref().getHost().toStringMessage(), proxyUri.ref().getPort().intValue());
         builder.setProxy(proxy);
      }

      int reqTimeout = options.ref().getRequestTimeout().intValue() * 1000;

      if (reqTimeout > 0)
      {
         builder.setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(reqTimeout)
                  .setConnectionRequestTimeout(reqTimeout).build());
      }

      return builder.build();
   }

   /**
    * Set request headers
    * 
    * @param req HTTP request
    * @param requestBuilder Apache HTTP request builder
    */
   private void setHeaders(object<? extends IhttpRequest> req,
                           org.apache.http.client.methods.RequestBuilder requestBuilder,
                           object[][] headers)
   {
      int nh = req.ref().getHeaders(
            new OutputExtentParameter<object<? extends HttpHeader>>()
            {
               @Override
               public object<? extends HttpHeader>[] getVariable()
               {
                  return headers[0];
               }

               @Override
               public void setVariable(object<? extends HttpHeader>[] reference)
               {
                  headers[0] = reference;
               }
            }).intValue();
      Set<String> unsupported = new HashSet<>(Arrays.asList("Content-Length", "Transfer-Encoding"));
      for (int hn = 0; hn < nh; hn++)
      {
         HttpHeader hdr = (HttpHeader) headers[0][hn].ref();
         if (!hdr.getName().isUnknown() && unsupported.contains(hdr.getName().toJavaType()))
         {
            LOG.log(Level.WARNING, 
                    "FWD HttpClient does not allow header " + hdr.getName().toJavaType() + 
                    " - will not be sent to the request.");
            continue;
         }
         String hdrParams = hdr.getParameterValues().getValue();
         
         String value = hdrParams.isEmpty() ? hdr.getValue().getValue() : hdr.getBaseValue().getValue() + hdrParams;
         
         requestBuilder.addHeader(hdr.getName().getValue(), value);
      }
   }

   /**
    * Create request body
    *  
    * @param req HTTP request
    * 
    * @return Apache HTTP request entity
    */
   private HttpEntity createEntity(object<? extends IhttpRequest> req)
   {
      object<? extends _BaseObject_> entity = req.ref().getEntity();
      if (entity.isValid().booleanValue())
      {
         object<? extends ByteBucket> bb = null;
         object<? extends MessageWriter> writer = null;
         try
         {
            character boundary = TypeFactory.character();
            if (ObjectOps.typeOf(entity, ISupportMultipartEntity.class).booleanValue())
            {
                boundary.assign(
                     ObjectOps.cast(entity, ISupportMultipartEntity.class).ref().getBoundary()
               );
               req.ref().getHeader(new character("Content-Type")).ref().
                  setParameterValue(new character("boundary"), boundary);
            }
            bb = ByteBucket.instance();
            writer = BodyWriterBuilder.build_1(req).ref().writeTo(bb).ref().getWriter();
            if (ObjectOps.typeOf(writer, ISupportMultipartEntity.class).booleanValue())
            {
               ObjectOps.cast(writer, ISupportMultipartEntity.class).ref().
                  setBoundary(boundary);
            }
            writer.ref().open();
            writer.ref().write(entity);
            writer.ref().close();
            int64 clen = bb.ref().getSize();
            int64 ptr = bb.ref().getBytes().ref().getPointerValue();
            memptr mptr = TypeFactory.memptr();
            mptr.setPointerValue(ptr);
            mptr.setLength(clen);
            return EntityBuilder.create().setBinary(mptr.getByteArray()).build(); 
         }
         finally
         {
            if (bb != null && bb._isValid())
            {
               ObjectOps.delete(bb);
            }
            
            if (writer != null && writer._isValid())
            {
               ObjectOps.delete(writer);
            }
         }
      }
      return EntityBuilder.create().setBinary(new byte[0]).build();
   }
   
   /**
    * Extract Entity from the response
    *   
    * @param response Apache HTTP response 
    * @param rsp OE HTTP response
    * 
    * @throws UnsupportedOperationException
    * @throws IOException
    */
   private void extractEntity(HttpResponse response, object<? extends IhttpResponse> rsp, object<? extends MessageWriter> entityWriter) 
   throws UnsupportedOperationException, IOException
   {
      object<? extends ByteBucket> rspBody = ByteBucket.instance();
      try
      {
         InputStream is = response.getEntity().getContent();
         byte[] bb = new byte[BUFSIZE];
         int len = is.read(bb);
         while(len >= 0)
         {
            if (len > 0)
            {
               rspBody.ref().putBytes(new memptr(Arrays.copyOf(bb, len)));
            }
            len = is.read(bb);
         }
         is.close();
         entityWriter.assign(EntityWriterBuilder.build_1(rsp).ref().getWriter());
         if (!entityWriter.isValid().booleanValue())
         {
            return;
         }
         integer clen = TypeFactory.integer();
         entityWriter.ref().open();
         clen.assign(entityWriter.ref().write(rspBody));
         entityWriter.ref().close();
         rsp.ref().setEntity(entityWriter.ref().getEntity());
         rsp.ref().setContentLength(clen);
      }
      finally
      {
         if (rspBody._isValid())
         {
            ObjectOps.delete(rspBody);
         }
      }
   }

   /**
    * Extract Headers from the response
    *   
    * @param response Apache HTTP response 
    * @param rsp OE HTTP response
    */
   private void extractHeaders(HttpResponse response, IhttpResponse rsp)
   {
      Arrays.stream(response.getAllHeaders())
         .filter(hdr -> !hdr.getName().equalsIgnoreCase("set-cookie"))
         .map(this::makeHeader)
         .forEach(rsp::setHeader);
      
      object<? extends HttpHeader> contentType = 
            rsp.getHeader(new character(HttpHeaders.CONTENT_TYPE));
      if (!ObjectOps.typeOf(contentType, NullHeader.class).booleanValue())
      {
         rsp.setContentType(contentType.ref().getValue());
      }
   }
   
   /**
    * Create OE HTTP header from Apache one
    * 
    * @param hdr Apache HTTP Header
    * 
    * @return OE HTTP Header
    */
   private object<? extends HttpHeader> makeHeader(Header hdr)
   {
      object<? extends HttpHeaderBuilder> builder = 
            HttpHeaderBuilder.build(new character(hdr.getName()));
      builder.assign(builder.ref().value(new character(hdr.getValue())));
      return builder.ref().getHeader();
   }

   /**
    * Extract payload from the HTTP response
    * 
    * @param response Apache HTTP response
    * @param contentType response content type
    * 
    * @return response payload
    * @throws IOException
    */
   private object<? extends _BaseObject_> getPayload(HttpResponse response, String contentType) 
         throws IOException
   {
      int paramPos = contentType.indexOf(';');
      if (paramPos > 0)
      {
         contentType = contentType.substring(0, paramPos);
      }
      HttpEntity entity = response.getEntity();
      switch (contentType)
      {
      case "application/json":
         String json = EntityUtils.toString(entity); 
         object<? extends ObjectModelParser> parser =  
               ObjectOps.newInstance(ObjectModelParser.class);
         return parser.ref().parse(new longchar(json));
      case "text/xml":
         byte[] xml = EntityUtils.toByteArray(entity);
         memptr ptr = new memptr(xml);
         XDocumentImpl doc = new XDocumentImpl();
         doc.load("memptr", ptr, false);
         return ObjectOps.newInstance(WidgetHandle.class, "I", new handle(doc));
      case "text/plain":
         String text = EntityUtils.toString(entity); 
         return ObjectOps.newInstance(LegacyString.class, "I", new longchar(text));
      case "application/octet-stream":
      default:
         raw bytes = new raw(EntityUtils.toByteArray(entity));
         return ObjectOps.newInstance(Memptr.class, "I", bytes);
      }
   }
   
   /**
    * Extract Cookies from the response
    *   
    * @param response Apache HTTP response 
    * @param rsp OE HTTP response
    */
   private void extractCookies(HttpResponse response, IhttpResponse rsp)
   {
      List<Header> headerList = Arrays.asList(response.getAllHeaders());
      
      List<Header> cookieHeaders = headerList.stream()
               .filter(p -> p.getName().equalsIgnoreCase("set-cookie"))
               .collect(Collectors.toList());
           
      cookieHeaders
         .stream()
         .map(cookieHeader -> makeCookie(cookieHeader))
         .forEach(parsedCookie -> rsp.setCookie(parsedCookie));
      
   }
   
   /**
    * Create OE HTTP Cookie from Apache one
    * 
    * @param hdr Apache HTTP Header
    * 
    * @return OE HTTP Cookie
    */
   private object<? extends Cookie> makeCookie(Header hdr)
   {      
      object<? extends Cookie> buildCookie = 
               Cookie.parse(new character(hdr.getValue()));

      return buildCookie;
   }

}