Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@

// By default no special SSL context present
private HttpClientFactory m_aHttpClientFactory = createDefaultHttpClientFactory ();
private HttpClientManager m_aSharedHttpClientManager;
private Consumer <? super HttpPost> m_aHttpCustomizer;
private boolean m_bQuoteHttpHeaders = DEFAULT_QUOTE_HTTP_HEADERS;

Expand All @@ -100,6 +101,35 @@
return this;
}

/**
* @return The shared HTTP client manager used for all requests, or <code>null</code> if every
* request should use a new manager. The returned manager is never closed by this class.
* @since 4.5.5
*/
@Nullable
public final HttpClientManager getSharedHttpClientManager ()
{
return m_aSharedHttpClientManager;
}

/**
* Set a shared HTTP client manager to reuse connections across requests. The caller owns the
* manager and must close it after this poster is no longer used. A shared manager must not be
* closed while requests are running. Pass <code>null</code> to restore the default behavior of
* creating and closing a manager per request.
*
* @param aSharedHttpClientManager
* The shared HTTP client manager to use. May be <code>null</code>.
* @return this for chaining
* @since 4.5.5
*/
@NonNull
public final BasicHttpPoster setSharedHttpClientManager (@Nullable @WillNotClose final HttpClientManager aSharedHttpClientManager)
{
m_aSharedHttpClientManager = aSharedHttpClientManager;
return this;
}

@Nullable
public final Consumer <? super HttpPost> getHttpCustomizer ()
{
Expand Down Expand Up @@ -147,7 +177,7 @@
*/
@Nullable
@Deprecated (forRemoval = true, since = "4.5.1")
public <T> T sendGenericMessage (@NonNull @Nonempty final String sURL,

Check warning on line 180 in phase4-lib/src/main/java/com/helger/phase4/messaging/http/BasicHttpPoster.java

View workflow job for this annotation

GitHub Actions / Java 21 build

<T>sendGenericMessage(@org.jspecify.annotations.NonNull,@com.helger.annotation.Nonempty java.lang.String,@org.jspecify.annotations.Nullable com.helger.http.header.HttpHeaderMap,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.HttpEntity,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.io.HttpClientResponseHandler<? extends T>) in com.helger.phase4.messaging.http.IHttpPoster has been deprecated and marked for removal

Check warning on line 180 in phase4-lib/src/main/java/com/helger/phase4/messaging/http/BasicHttpPoster.java

View workflow job for this annotation

GitHub Actions / Java 25 build

<T>sendGenericMessage(@org.jspecify.annotations.NonNull @com.helger.annotation.Nonempty java.lang.String,@org.jspecify.annotations.Nullable com.helger.http.header.HttpHeaderMap,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.HttpEntity,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.io.HttpClientResponseHandler<? extends T>) in com.helger.phase4.messaging.http.IHttpPoster has been deprecated and marked for removal

Check warning on line 180 in phase4-lib/src/main/java/com/helger/phase4/messaging/http/BasicHttpPoster.java

View workflow job for this annotation

GitHub Actions / Java 17 build

<T>sendGenericMessage(@org.jspecify.annotations.NonNull,@com.helger.annotation.Nonempty java.lang.String,@org.jspecify.annotations.Nullable com.helger.http.header.HttpHeaderMap,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.HttpEntity,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.io.HttpClientResponseHandler<? extends T>) in com.helger.phase4.messaging.http.IHttpPoster has been deprecated and marked for removal
@Nullable final HttpHeaderMap aCustomHttpHeaders,
@NonNull final HttpEntity aHttpEntity,
@NonNull final HttpClientResponseHandler <? extends T> aResponseHandler) throws IOException
Expand Down Expand Up @@ -194,58 +224,25 @@
LOGGER.info ("Starting to transmit AS4 Message to '" + sURL + "'");

IOException aCaughtException = null;
try (final HttpClientManager aClientMgr = new HttpClientManager (m_aHttpClientFactory))
try
{
final HttpPost aPost = new HttpPost (sURL);

if (aCustomHttpHeaders != null)
if (m_aSharedHttpClientManager != null)
return _sendGenericMessageWithClientManager (sURL,
aCustomHttpHeaders,
aHttpEntity,
aResponseHandler,
aRemoteTlsPeerCertConsumer,
m_aSharedHttpClientManager);

try (final HttpClientManager aClientMgr = new HttpClientManager (m_aHttpClientFactory))
{
// Always unify line endings
// By default quoting is disabled
aCustomHttpHeaders.forEachSingleHeader (aPost::addHeader, true, m_bQuoteHttpHeaders);
return _sendGenericMessageWithClientManager (sURL,
aCustomHttpHeaders,
aHttpEntity,
aResponseHandler,
aRemoteTlsPeerCertConsumer,
aClientMgr);
}

aPost.setEntity (aHttpEntity);

// Invoke optional customizer
if (m_aHttpCustomizer != null)
m_aHttpCustomizer.accept (aPost);

// Debug sending
AS4HttpDebug.debug ( () -> {
final StringBuilder ret = new StringBuilder ("SEND-START to ").append (sURL).append ("\n");
try
{
for (final Header aHeader : aPost.getHeaders ())
ret.append (aHeader.getName ()).append (": ").append (aHeader.getValue ()).append (CHttp.EOL);
ret.append (CHttp.EOL);
if (aHttpEntity.isRepeatable ())
ret.append (EntityUtils.toString (aHttpEntity));
else
ret.append ("## The payload is marked as 'not repeatable' and is the therefore not printed in debugging");
}
catch (final Exception ex)
{
ret.append ("## Exception listing payload: " + ex.getClass ().getName () + " -- " + ex.getMessage ())
.append (CHttp.EOL);
ret.append ("## ").append (StackTraceHelper.getStackAsString (ex));
}
return ret.toString ();
});

// Execute main HTTP request
final HttpClientContext aHttpClientContext = HttpClientContext.create ();
final T ret = aClientMgr.execute (aPost, aHttpClientContext, aResponseHandler);

// Surface the TLS peer (server) certificates if requested. The
// CapturingTlsSocketStrategy is wired in by HttpClientFactory by default.
if (aRemoteTlsPeerCertConsumer != null)
{
final ICommonsList <X509Certificate> aRemoteTlsCerts = CapturingTlsSocketStrategy.getRemoteTLSCertificates (aHttpClientContext);
aRemoteTlsPeerCertConsumer.accept (aRemoteTlsCerts);
}

return ret;
}
catch (final IOException ex)
{
Expand All @@ -272,6 +269,66 @@
}
}

@Nullable
private <T> T _sendGenericMessageWithClientManager (@NonNull @Nonempty final String sURL,
@Nullable final HttpHeaderMap aCustomHttpHeaders,
@NonNull final HttpEntity aHttpEntity,
@NonNull final HttpClientResponseHandler <? extends T> aResponseHandler,
@Nullable final Consumer <? super ICommonsList <X509Certificate>> aRemoteTlsPeerCertConsumer,
@NonNull final HttpClientManager aClientMgr) throws IOException
{
final HttpPost aPost = new HttpPost (sURL);

if (aCustomHttpHeaders != null)
{
// Always unify line endings
// By default quoting is disabled
aCustomHttpHeaders.forEachSingleHeader (aPost::addHeader, true, m_bQuoteHttpHeaders);
}

aPost.setEntity (aHttpEntity);

// Invoke optional customizer
if (m_aHttpCustomizer != null)
m_aHttpCustomizer.accept (aPost);

// Debug sending
AS4HttpDebug.debug ( () -> {
final StringBuilder ret = new StringBuilder ("SEND-START to ").append (sURL).append ("\n");
try
{
for (final Header aHeader : aPost.getHeaders ())
ret.append (aHeader.getName ()).append (": ").append (aHeader.getValue ()).append (CHttp.EOL);
ret.append (CHttp.EOL);
if (aHttpEntity.isRepeatable ())
ret.append (EntityUtils.toString (aHttpEntity));
else
ret.append ("## The payload is marked as 'not repeatable' and is the therefore not printed in debugging");
}
catch (final Exception ex)
{
ret.append ("## Exception listing payload: " + ex.getClass ().getName () + " -- " + ex.getMessage ())
.append (CHttp.EOL);
ret.append ("## ").append (StackTraceHelper.getStackAsString (ex));
}
return ret.toString ();
});

// Execute main HTTP request
final HttpClientContext aHttpClientContext = HttpClientContext.create ();
final T ret = aClientMgr.execute (aPost, aHttpClientContext, aResponseHandler);

// Surface the TLS peer (server) certificates if requested. The
// CapturingTlsSocketStrategy is wired in by HttpClientFactory by default.
if (aRemoteTlsPeerCertConsumer != null)
{
final ICommonsList <X509Certificate> aRemoteTlsCerts = CapturingTlsSocketStrategy.getRemoteTLSCertificates (aHttpClientContext);
aRemoteTlsPeerCertConsumer.accept (aRemoteTlsCerts);
}

return ret;
}

@NonNull
protected static HttpEntity createDumpingHttpEntity (@Nullable final IAS4OutgoingDumper aOutgoingDumper,
@NonNull final HttpEntity aSrcEntity,
Expand Down Expand Up @@ -327,7 +384,7 @@

@Nullable
@Deprecated (forRemoval = true, since = "4.5.1")
public <T> T sendGenericMessageWithRetries (@NonNull final String sURL,

Check warning on line 387 in phase4-lib/src/main/java/com/helger/phase4/messaging/http/BasicHttpPoster.java

View workflow job for this annotation

GitHub Actions / Java 21 build

<T>sendGenericMessageWithRetries(@org.jspecify.annotations.NonNull java.lang.String,@org.jspecify.annotations.Nullable com.helger.http.header.HttpHeaderMap,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.HttpEntity,@org.jspecify.annotations.NonNull java.lang.String,@org.jspecify.annotations.NonNull com.helger.phase4.messaging.http.HttpRetrySettings,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.io.HttpClientResponseHandler<? extends T>,@org.jspecify.annotations.Nullable com.helger.phase4.dump.IAS4OutgoingDumper,@org.jspecify.annotations.Nullable com.helger.phase4.client.IAS4RetryCallback) in com.helger.phase4.messaging.http.IHttpPoster has been deprecated and marked for removal

Check warning on line 387 in phase4-lib/src/main/java/com/helger/phase4/messaging/http/BasicHttpPoster.java

View workflow job for this annotation

GitHub Actions / Java 25 build

<T>sendGenericMessageWithRetries(@org.jspecify.annotations.NonNull java.lang.String,@org.jspecify.annotations.Nullable com.helger.http.header.HttpHeaderMap,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.HttpEntity,@org.jspecify.annotations.NonNull java.lang.String,@org.jspecify.annotations.NonNull com.helger.phase4.messaging.http.HttpRetrySettings,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.io.HttpClientResponseHandler<? extends T>,@org.jspecify.annotations.Nullable com.helger.phase4.dump.IAS4OutgoingDumper,@org.jspecify.annotations.Nullable com.helger.phase4.client.IAS4RetryCallback) in com.helger.phase4.messaging.http.IHttpPoster has been deprecated and marked for removal

Check warning on line 387 in phase4-lib/src/main/java/com/helger/phase4/messaging/http/BasicHttpPoster.java

View workflow job for this annotation

GitHub Actions / Java 17 build

<T>sendGenericMessageWithRetries(@org.jspecify.annotations.NonNull java.lang.String,@org.jspecify.annotations.Nullable com.helger.http.header.HttpHeaderMap,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.HttpEntity,@org.jspecify.annotations.NonNull java.lang.String,@org.jspecify.annotations.NonNull com.helger.phase4.messaging.http.HttpRetrySettings,@org.jspecify.annotations.NonNull org.apache.hc.core5.http.io.HttpClientResponseHandler<? extends T>,@org.jspecify.annotations.Nullable com.helger.phase4.dump.IAS4OutgoingDumper,@org.jspecify.annotations.Nullable com.helger.phase4.client.IAS4RetryCallback) in com.helger.phase4.messaging.http.IHttpPoster has been deprecated and marked for removal
@Nullable final HttpHeaderMap aCustomHttpHeaders,
@NonNull final HttpEntity aHttpEntity,
@NonNull final String sMessageID,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2026 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.helger.phase4.messaging.http;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.junit.Test;

import com.helger.httpclient.HttpClientFactory;
import com.helger.httpclient.HttpClientManager;
import com.sun.net.httpserver.HttpServer;

/**
* Test class for class {@link BasicHttpPoster}.
*
* @author Greg Taube
*/
public final class BasicHttpPosterTest
{
@Test
public void testSharedHttpClientManagerReusesConnection () throws IOException
{
final Set <Integer> aRemotePorts = ConcurrentHashMap.newKeySet ();
final HttpServer aServer = HttpServer.create (new InetSocketAddress ("127.0.0.1", 0), 0);
aServer.createContext ("/", aExchange -> {
aRemotePorts.add (Integer.valueOf (aExchange.getRemoteAddress ().getPort ()));
aExchange.getRequestBody ().readAllBytes ();
final byte [] aResponse = "ok".getBytes (StandardCharsets.UTF_8);
aExchange.sendResponseHeaders (200, aResponse.length);
aExchange.getResponseBody ().write (aResponse);
aExchange.close ();
});
aServer.start ();

final HttpClientManager aHttpClientManager = new HttpClientManager (new HttpClientFactory ());
try
{
final BasicHttpPoster aPoster = new BasicHttpPoster ();
assertNull (aPoster.getSharedHttpClientManager ());
assertSame (aPoster, aPoster.setSharedHttpClientManager (aHttpClientManager));
assertSame (aHttpClientManager, aPoster.getSharedHttpClientManager ());

final String sURL = "http://127.0.0.1:" + aServer.getAddress ().getPort () + '/';
assertEquals ("ok", _send (aPoster, sURL));
assertEquals ("ok", _send (aPoster, sURL));

assertEquals (1, aRemotePorts.size ());
assertFalse (aHttpClientManager.isClosed ());
}
finally
{
aHttpClientManager.close ();
aServer.stop (0);
}
}

private static String _send (final BasicHttpPoster aPoster, final String sURL) throws IOException
{
return aPoster.sendGenericMessage (sURL,
null,
new StringEntity ("request", ContentType.TEXT_PLAIN),
aResponse -> EntityUtils.toString (aResponse.getEntity ()),
null);
}
}