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

    }