This is an automated email from the ASF dual-hosted git repository.

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git

commit 8f1f38a1da7fd6297607e4b53779ecdfec4a923d
Author: Robert Lazarski <[email protected]>
AuthorDate: Sun Apr 19 18:17:44 2026 -1000

    AXIS2-6103 Add HTTP/2 Java client example and documentation
    
    New Http2JsonClient.java in userguide samples — standalone Apache
    HttpClient 5 HTTP/2 client for Axis2 JSON-RPC services. Two modes:
    buffered (returns String) and streaming (writes 64KB chunks to
    OutputStream). Includes timeout cancellation and interrupt handling.
    
    New http2-java-client.xml xdoc page with usage examples for both
    modes, dependency info, and end-to-end streaming flow diagram.
    
    Linked in toc.xml as section 18.4a under Transports.
---
 .../springboot/client/Http2JsonClient.java         | 288 +++++++++++++++++++++
 src/site/xdoc/docs/http2-java-client.xml           | 159 ++++++++++++
 src/site/xdoc/docs/toc.xml                         |  21 +-
 3 files changed, 458 insertions(+), 10 deletions(-)

diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java
new file mode 100644
index 0000000000..7a91dc9fdf
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 userguide.springboot.client;
+
+import javax.net.ssl.SSLContext;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import 
org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.TrustAllStrategy;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.apache.hc.core5.util.Timeout;
+
+/**
+ * Standalone HTTP/2 JSON client for Apache Axis2 services.
+ *
+ * <p>Uses Apache HttpClient 5 async with HTTP/2 (ALPN over TLS) to call
+ * any Axis2 JSON-RPC endpoint. Two execution modes:</p>
+ *
+ * <ul>
+ *   <li>{@link #execute} — returns the full response as a String</li>
+ *   <li>{@link #executeStreaming} — writes response bytes to an OutputStream
+ *       in 64KB chunks as HTTP/2 DATA frames arrive; memory stays flat</li>
+ * </ul>
+ *
+ * <p>Requires: Java 11+, Apache HttpClient 5.4+ (httpcore5-h2 for HTTP/2).</p>
+ *
+ * <h3>Quick start</h3>
+ * <pre>
+ * // 1. POST JSON-RPC, get response as String
+ * String response = Http2JsonClient.execute(
+ *     
"https://localhost:8443/axis2-json-api/services/FinancialBenchmarkService";,
+ *     "{\"monteCarlo\":[{\"arg0\":{\"nSimulations\":100000}}]}",
+ *     300);
+ *
+ * // 2. Stream a large response to a file
+ * try (FileOutputStream fos = new FileOutputStream("/tmp/result.json")) {
+ *     int status = Http2JsonClient.executeStreaming(
+ *         "https://localhost:8443/axis2-json-api/services/BigDataH2Service";,
+ *         "{\"generate\":[{\"arg0\":{\"datasetSize\":52428800}}]}",
+ *         300, fos);
+ * }
+ *
+ * // 3. Shutdown when done
+ * Http2JsonClient.shutdown();
+ * </pre>
+ *
+ * @see <a 
href="https://axis.apache.org/axis2/java/core/docs/http2-integration-guide.html";>
+ *      Axis2 HTTP/2 Integration Guide</a>
+ */
+public class Http2JsonClient {
+
+    private static volatile CloseableHttpAsyncClient sharedClient;
+
+    /**
+     * Get or create the shared HTTP/2 async client.
+     *
+     * <p>Initialized once, reused for all requests. Uses TLS with
+     * TrustAllStrategy for development — replace with proper trust
+     * material in production.</p>
+     */
+    private static synchronized CloseableHttpAsyncClient getClient() throws 
Exception {
+        if (sharedClient == null) {
+            H2Config h2Config = H2Config.custom()
+                .setMaxConcurrentStreams(100)
+                .setPushEnabled(false)
+                .setInitialWindowSize(65536)
+                .build();
+
+            IOReactorConfig ioConfig = IOReactorConfig.custom()
+                .setTcpNoDelay(true)
+                .setSoTimeout(Timeout.ofMinutes(5))
+                .build();
+
+            SSLContext sslContext = SSLContexts.custom()
+                .loadTrustMaterial(null, TrustAllStrategy.INSTANCE)
+                .build();
+
+            PoolingAsyncClientConnectionManager connManager =
+                PoolingAsyncClientConnectionManagerBuilder.create()
+                    .setTlsStrategy(ClientTlsStrategyBuilder.create()
+                        .setSslContext(sslContext)
+                        .setHostnameVerifier(NoopHostnameVerifier.INSTANCE)
+                        .build())
+                    .setMaxConnTotal(50)
+                    .setMaxConnPerRoute(10)
+                    .build();
+
+            sharedClient = HttpAsyncClients.custom()
+                .setH2Config(h2Config)
+                .setIOReactorConfig(ioConfig)
+                .setConnectionManager(connManager)
+                .build();
+
+            sharedClient.start();
+        }
+        return sharedClient;
+    }
+
+    /**
+     * POST JSON to an Axis2 service and return the response as a String.
+     *
+     * @param url            HTTPS endpoint (e.g., {@code 
https://host:8443/axis2-json-api/services/MyService})
+     * @param json           JSON-RPC request body
+     * @param timeoutSeconds maximum wait time for the response
+     * @return response body as a String
+     * @throws Exception on HTTP error or timeout
+     */
+    public static String execute(String url, String json, int timeoutSeconds) 
throws Exception {
+        CloseableHttpAsyncClient client = getClient();
+
+        SimpleHttpRequest request = SimpleRequestBuilder.post(url)
+            .setBody(json, ContentType.APPLICATION_JSON)
+            .setHeader("Accept", "application/json")
+            .build();
+
+        CompletableFuture<SimpleHttpResponse> future = new 
CompletableFuture<>();
+        Future<SimpleHttpResponse> requestFuture = client.execute(request,
+            new FutureCallback<SimpleHttpResponse>() {
+                @Override public void completed(SimpleHttpResponse r) { 
future.complete(r); }
+                @Override public void failed(Exception ex) { 
future.completeExceptionally(ex); }
+                @Override public void cancelled() { future.cancel(true); }
+            });
+
+        SimpleHttpResponse response;
+        try {
+            response = future.get(timeoutSeconds, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            requestFuture.cancel(true);
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            throw e;
+        }
+
+        int status = response.getCode();
+        if (status < 200 || status >= 300) {
+            throw new java.io.IOException("HTTP " + status + ": "
+                + response.getBodyText().substring(0, Math.min(500,
+                    response.getBodyText() != null ? 
response.getBodyText().length() : 0)));
+        }
+        return response.getBodyText();
+    }
+
+    /**
+     * POST JSON to an Axis2 service and stream the response to an 
OutputStream.
+     *
+     * <p>Instead of buffering the entire response in memory, writes 64KB 
chunks
+     * to the provided OutputStream as HTTP/2 DATA frames arrive. Memory stays
+     * flat regardless of response size.</p>
+     *
+     * <p>When paired with Axis2's
+     * {@code MoshiStreamingMessageFormatter} (AXIS2-6103), data flows
+     * end-to-end in 64KB chunks: server flushes → HTTP/2 DATA frames →
+     * this callback → your OutputStream.</p>
+     *
+     * @param url            HTTPS endpoint
+     * @param json           JSON-RPC request body
+     * @param timeoutSeconds maximum wait time for the response
+     * @param outputStream   destination for response bytes
+     * @return HTTP status code
+     * @throws Exception on HTTP error or timeout
+     */
+    public static int executeStreaming(String url, String json,
+                                       int timeoutSeconds, OutputStream 
outputStream) throws Exception {
+        CloseableHttpAsyncClient client = getClient();
+
+        org.apache.hc.core5.http.nio.AsyncRequestProducer requestProducer =
+            org.apache.hc.core5.http.nio.support.AsyncRequestBuilder.post(url)
+                .setEntity(json, ContentType.APPLICATION_JSON)
+                .addHeader("Accept", "application/json")
+                .build();
+
+        final long[] totalBytes = {0};
+        final int[] chunkCount = {0};
+        final int[] statusCode = {0};
+
+        
org.apache.hc.client5.http.async.methods.AbstractBinResponseConsumer<Integer> 
consumer =
+            new 
org.apache.hc.client5.http.async.methods.AbstractBinResponseConsumer<Integer>() 
{
+
+                private byte[] transferBuffer;
+
+                @Override
+                protected void start(org.apache.hc.core5.http.HttpResponse 
response,
+                                     org.apache.hc.core5.http.ContentType 
contentType)
+                        throws org.apache.hc.core5.http.HttpException, 
java.io.IOException {
+                    statusCode[0] = response.getCode();
+                }
+
+                @Override
+                protected int capacityIncrement() {
+                    return 64 * 1024;
+                }
+
+                @Override
+                protected void data(java.nio.ByteBuffer src, boolean 
endOfStream)
+                        throws java.io.IOException {
+                    int len = src.remaining();
+                    if (len > 0) {
+                        if (src.hasArray()) {
+                            outputStream.write(src.array(), src.arrayOffset() 
+ src.position(), len);
+                            src.position(src.position() + len);
+                        } else {
+                            if (transferBuffer == null || 
transferBuffer.length < len) {
+                                transferBuffer = new byte[len];
+                            }
+                            src.get(transferBuffer, 0, len);
+                            outputStream.write(transferBuffer, 0, len);
+                        }
+                        outputStream.flush();
+                        totalBytes[0] += len;
+                        chunkCount[0]++;
+                    }
+                }
+
+                @Override
+                protected Integer buildResult() { return statusCode[0]; }
+
+                @Override
+                public void releaseResources() { }
+            };
+
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        Future<Integer> requestFuture = client.execute(requestProducer, 
consumer,
+            new FutureCallback<Integer>() {
+                @Override public void completed(Integer r) { 
future.complete(r); }
+                @Override public void failed(Exception ex) { 
future.completeExceptionally(ex); }
+                @Override public void cancelled() { future.cancel(true); }
+            });
+
+        int result;
+        try {
+            result = future.get(timeoutSeconds, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            requestFuture.cancel(true);
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            throw e;
+        }
+
+        return result;
+    }
+
+    /**
+     * Shut down the shared HTTP/2 client. Call once at application exit.
+     */
+    public static void shutdown() {
+        if (sharedClient != null) {
+            try {
+                sharedClient.close();
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+    }
+}
diff --git a/src/site/xdoc/docs/http2-java-client.xml 
b/src/site/xdoc/docs/http2-java-client.xml
new file mode 100644
index 0000000000..61d57ca593
--- /dev/null
+++ b/src/site/xdoc/docs/http2-java-client.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements. See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership. The ASF licenses this file
+  ~ to you 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.
+  -->
+<document xmlns="http://maven.apache.org/XDOC/2.0";
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+          xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 
http://maven.apache.org/xsd/xdoc-2.0.xsd";>
+
+    <properties>
+        <title>HTTP/2 Java Client for Axis2 JSON-RPC Services</title>
+    </properties>
+
+    <body>
+        <section name="HTTP/2 Java Client">
+
+        <p>A standalone Java client for calling Axis2 JSON-RPC services over
+        HTTP/2. Uses Apache HttpClient 5 async with ALPN negotiation over 
TLS.</p>
+
+        <p>Two execution modes:</p>
+        <ul>
+            <li><strong>Buffered</strong> — returns the full response as a
+                <code>String</code>. Simple, suitable for responses that fit
+                in memory.</li>
+            <li><strong>Streaming</strong> — writes response bytes to an
+                <code>OutputStream</code> in 64KB chunks as HTTP/2 DATA
+                frames arrive. Memory stays flat regardless of response size.
+                When paired with the
+                <a href="json-streaming-formatter.html">Streaming JSON
+                Formatter</a> (AXIS2-6103), data flows end-to-end in 64KB
+                chunks.</li>
+        </ul>
+
+        </section>
+
+        <section name="Dependencies">
+
+        <p>Requires Java 11+ and Apache HttpClient 5.4+:</p>
+
+<source>
+&lt;!-- Maven --&gt;
+&lt;dependency&gt;
+    &lt;groupId&gt;org.apache.httpcomponents.client5&lt;/groupId&gt;
+    &lt;artifactId&gt;httpclient5&lt;/artifactId&gt;
+    &lt;version&gt;5.4.3&lt;/version&gt;
+&lt;/dependency&gt;
+&lt;dependency&gt;
+    &lt;groupId&gt;org.apache.httpcomponents.core5&lt;/groupId&gt;
+    &lt;artifactId&gt;httpcore5-h2&lt;/artifactId&gt;
+    &lt;version&gt;5.4.1&lt;/version&gt;
+&lt;/dependency&gt;
+</source>
+
+        </section>
+
+        <section name="Buffered Execution">
+
+        <p>POST JSON-RPC to any Axis2 service, get the response as a 
String:</p>
+
+<source>
+String url = 
"https://localhost:8443/axis2-json-api/services/FinancialBenchmarkService";;
+String json = 
"{\"monteCarlo\":[{\"arg0\":{\"nSimulations\":100000,\"nPeriods\":252,"
+    + "\"initialValue\":1000000,\"expectedReturn\":0.10,\"volatility\":0.223,"
+    + "\"nPeriodsPerYear\":252,\"randomSeed\":42}}]}";
+
+String response = Http2JsonClient.execute(url, json, 300);
+System.out.println(response);
+
+Http2JsonClient.shutdown();
+</source>
+
+        <p>The client negotiates HTTP/2 via ALPN on the TLS handshake.
+        Connections are pooled and multiplexed — multiple concurrent requests
+        share a single TCP connection.</p>
+
+        </section>
+
+        <section name="Streaming Execution">
+
+        <p>For large responses (10MB+), stream to a file or parser instead
+        of buffering in heap:</p>
+
+<source>
+String url = "https://localhost:8443/axis2-json-api/services/BigDataH2Service";;
+String json = "{\"generate\":[{\"arg0\":{\"datasetSize\":52428800}}]}";
+
+try (FileOutputStream fos = new FileOutputStream("/tmp/result.json")) {
+    int status = Http2JsonClient.executeStreaming(url, json, 300, fos);
+    System.out.println("HTTP " + status);
+}
+
+Http2JsonClient.shutdown();
+</source>
+
+        <p>Each HTTP/2 DATA frame triggers a callback that writes directly
+        to your <code>OutputStream</code>. The <code>capacityIncrement()</code>
+        returns 64KB, creating natural HTTP/2 flow control backpressure —
+        the client tells the server "I can accept 64KB more" after each
+        chunk.</p>
+
+        <p>When the server uses the
+        <a href="json-streaming-formatter.html">Streaming JSON Formatter</a>,
+        data flows end-to-end without full-body buffering on either side:</p>
+
+<source>
+Server (MoshiStreamingMessageFormatter)
+  → FlushingOutputStream flushes every 64KB
+  → HTTP/2 DATA frames
+  → Http2JsonClient.data() callback
+  → your OutputStream
+</source>
+
+        </section>
+
+        <section name="Timeout and Cancellation">
+
+        <p>Both methods accept a <code>timeoutSeconds</code> parameter for
+        <code>Future.get()</code>. If the timeout expires or the thread is
+        interrupted, the underlying HTTP request is cancelled to prevent
+        zombie requests that would continue consuming resources:</p>
+
+<source>
+try {
+    response = future.get(timeoutSeconds, TimeUnit.SECONDS);
+} catch (Exception e) {
+    requestFuture.cancel(true);  // Cancel the HTTP request
+    if (e instanceof InterruptedException) {
+        Thread.currentThread().interrupt();  // Restore interrupt flag
+    }
+    throw e;
+}
+</source>
+
+        </section>
+
+        <section name="Source Code">
+
+        <p>The complete client is available in the userguide samples:</p>
+
+        
<p><code>modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java</code></p>
+
+        </section>
+
+    </body>
+</document>
diff --git a/src/site/xdoc/docs/toc.xml b/src/site/xdoc/docs/toc.xml
index 754690cbe1..5559ce14f7 100644
--- a/src/site/xdoc/docs/toc.xml
+++ b/src/site/xdoc/docs/toc.xml
@@ -116,16 +116,17 @@ Guide</a></li>
 <li>Transports
 <ul>
 <li>18.1 <a href="http-transport.html">HTTP
-Transport sender</a></li>
-<li>18.2 <a href="axis2-http2-simplified-example.html">HTTP/2
-Simplified Configuration</a></li>
-<li>18.3 <a href="http2-transport-additions.html">HTTP/2
-Transport</a></li>
-<li>18.4 <a href="http2-integration-guide.html">HTTP/2
-Integration Guide</a></li>
-<li>18.5 <a href="wildfly-http2-integration-guide.html">WildFly HTTP/2
-Integration Guide</a></li>
-<li>18.6 <a href="json-streaming-formatter.html">Streaming JSON Formatter for 
Large HTTP/2 Responses</a></li>
+Transport sender</a> (HTTP/1.1)</li>
+<li>18.2 <a href="http2-integration-guide.html"><strong>HTTP/2
+Overview — start here</strong></a> (architecture, framework comparison, why 
Axis2 is different)</li>
+<li>18.3 <a href="axis2-http2-simplified-example.html">HTTP/2
+Simplified Configuration</a> (minimal axis2.xml — 2 parameters)</li>
+<li>18.4 <a href="json-streaming-formatter.html">Streaming JSON Formatter + 
Field Selection</a> (64 KB flush, <code>?fields=</code>)</li>
+<li>18.4a <a href="http2-java-client.html">HTTP/2 Java Client</a> (standalone 
HttpClient 5, buffered + streaming)</li>
+<li>18.5 <a href="wildfly-http2-integration-guide.html">WildFly HTTP/2</a> |
+<a href="tomcat-http2-integration-guide.html">Tomcat HTTP/2</a> 
(container-specific)</li>
+<li>18.6 <a href="http2-transport-additions.html">HTTP/2
+Transport Module Reference</a> (transport-h2 internals)</li>
 <li>18.7 <a href="servlet-transport.html">HTTP
 servlet transport</a></li>
 <li>18.8 <a href="jms-transport.html">JMS Transport</a></li>

Reply via email to