This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new d1e0e9a056 Next Generation RestClient
d1e0e9a056 is described below
commit d1e0e9a0563a52f27c2208faf8a6972f5b3553ca
Author: James Bognar <[email protected]>
AuthorDate: Thu Apr 9 15:36:17 2026 -0400
Next Generation RestClient
---
.../pom.xml | 88 +++++++
.../apachehttpclient50/ApacheHc5Transport.java | 143 +++++++++++
.../ApacheHc5TransportBuilder.java | 70 ++++++
.../ApacheHc5TransportProvider.java | 61 +++++
...che.juneau.ng.rest.client.HttpTransportProvider | 16 ++
.../juneau-ng-rest-client-java-httpclient/pom.xml | 83 +++++++
.../client/javahttpclient/JavaHttpTransport.java | 140 +++++++++++
.../javahttpclient/JavaHttpTransportBuilder.java | 67 +++++
.../javahttpclient/JavaHttpTransportProvider.java | 61 +++++
...che.juneau.ng.rest.client.HttpTransportProvider | 16 ++
juneau-rest/pom.xml | 2 +
juneau-utest/pom.xml | 10 +
.../juneau/ng/rest/ApacheHc5Transport_Test.java | 274 ++++++++++++++++++++
.../juneau/ng/rest/JavaHttpTransport_Test.java | 275 +++++++++++++++++++++
todo/restclient2-transport-abstraction.md | 2 +-
15 files changed, 1307 insertions(+), 1 deletion(-)
diff --git a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/pom.xml
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/pom.xml
new file mode 100644
index 0000000000..b2097f6b40
--- /dev/null
+++ b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/pom.xml
@@ -0,0 +1,88 @@
+<?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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest</artifactId>
+ <version>9.2.1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>juneau-ng-rest-client-apache-httpclient-50</artifactId>
+ <name>Apache Juneau NG REST Client — Apache HttpClient 5.x
Transport</name>
+ <description>Apache HttpClient 5.x transport adapter for the
next-generation Juneau REST client.</description>
+ <packaging>bundle</packaging>
+
+ <properties>
+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents.client5</groupId>
+ <artifactId>httpclient5</artifactId>
+ <version>5.5.2</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+
<supportIncrementalBuild>true</supportIncrementalBuild>
+ </configuration>
+ <executions>
+ <execution>
+ <id>bundle-manifest</id>
+ <phase>process-classes</phase>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5Transport.java
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5Transport.java
new file mode 100644
index 0000000000..95375a4167
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5Transport.java
@@ -0,0 +1,143 @@
+/*
+ * 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 org.apache.juneau.ng.rest.client.apachehttpclient50;
+
+import java.io.*;
+
+import org.apache.hc.client5.http.impl.classic.*;
+import org.apache.hc.core5.http.*;
+import org.apache.hc.core5.http.io.entity.*;
+import org.apache.hc.core5.http.io.support.*;
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransport} implementation backed by Apache HttpClient 5.x.
+ *
+ * <p>
+ * This transport is auto-discovered via {@link java.util.ServiceLoader} when
+ * {@code org.apache.httpcomponents.client5:httpclient5} is on the classpath.
You can also instantiate it explicitly:
+ *
+ * <p class='bjava'>
+ * <jv>transport</jv> = ApacheHc5Transport.<jsm>builder</jsm>()
+ * .httpClient(HttpClients.createDefault())
+ * .build();
+ *
+ * <jv>client</jv> = NgRestClient.<jsm>builder</jsm>()
+ * .transport(<jv>transport</jv>)
+ * .build();
+ * </p>
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client">juneau-ng
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+@SuppressWarnings({
+ "resource" // httpClient is owned by this transport and closed in
close()
+})
+public final class ApacheHc5Transport implements HttpTransport {
+
+ private final CloseableHttpClient httpClient;
+
+ ApacheHc5Transport(ApacheHc5TransportBuilder builder) {
+ this.httpClient = builder.httpClient != null ?
builder.httpClient : HttpClients.createDefault();
+ }
+
+ /**
+ * Returns a new builder for this transport.
+ *
+ * @return A new builder. Never <jk>null</jk>.
+ */
+ public static ApacheHc5TransportBuilder builder() {
+ return new ApacheHc5TransportBuilder();
+ }
+
+ /**
+ * Returns a new instance backed by a default {@link
CloseableHttpClient}.
+ *
+ * @return A new instance. Never <jk>null</jk>.
+ */
+ public static ApacheHc5Transport create() {
+ return builder().build();
+ }
+
+ @Override /* HttpTransport */
+ public TransportResponse execute(TransportRequest request) throws
TransportException {
+ var hcRequest = buildHcRequest(request);
+ CloseableHttpResponse hcResponse;
+ try {
+ hcResponse = httpClient.execute(hcRequest);
+ } catch (IOException e) {
+ throw new TransportException("HTTP transport error: " +
e.getMessage(), e);
+ }
+ return buildTransportResponse(hcResponse);
+ }
+
+ @Override /* Closeable */
+ public void close() throws IOException {
+ httpClient.close();
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // Internal helpers
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ private static ClassicHttpRequest buildHcRequest(TransportRequest
request) throws TransportException {
+ var builder =
ClassicRequestBuilder.create(request.getMethod()).setUri(request.getUri());
+ for (var h : request.getHeaders())
+ builder.addHeader(h.name(), h.value());
+ var body = request.getBody();
+ if (body != null)
+ builder.setEntity(buildEntity(body));
+ try {
+ return builder.build();
+ } catch (Exception e) {
+ throw new TransportException("Failed to build HTTP
request: " + e.getMessage(), e);
+ }
+ }
+
+ private static HttpEntity buildEntity(TransportBody body) {
+ var ct = body.getContentType();
+ var contentType = ct != null ? ContentType.parse(ct) :
ContentType.APPLICATION_OCTET_STREAM;
+ return new EntityTemplate(body.getContentLength(), contentType,
null, body::writeTo);
+ }
+
+ private static TransportResponse
buildTransportResponse(CloseableHttpResponse hcResponse) throws
TransportException {
+ var builder = TransportResponse.builder()
+ .statusCode(hcResponse.getCode())
+ .reasonPhrase(hcResponse.getReasonPhrase())
+ .closeCallback(hcResponse);
+ for (var h : hcResponse.getHeaders())
+ builder.header(h.getName(), h.getValue());
+ var entity = hcResponse.getEntity();
+ if (entity != null) {
+ try {
+ builder.body(entity.getContent());
+ } catch (IOException e) {
+ throw new TransportException("Failed to read
response body: " + e.getMessage(), e);
+ }
+ }
+ return builder.build();
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportBuilder.java
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportBuilder.java
new file mode 100644
index 0000000000..a4697c6146
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportBuilder.java
@@ -0,0 +1,70 @@
+/*
+ * 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 org.apache.juneau.ng.rest.client.apachehttpclient50;
+
+import org.apache.hc.client5.http.impl.classic.*;
+
+/**
+ * Fluent builder for {@link ApacheHc5Transport}.
+ *
+ * <p>
+ * Obtain an instance via {@link ApacheHc5Transport#builder()}.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client">juneau-ng
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class ApacheHc5TransportBuilder {
+
+ CloseableHttpClient httpClient;
+
+ ApacheHc5TransportBuilder() {}
+
+ /**
+ * Sets the underlying {@link CloseableHttpClient} to use.
+ *
+ * <p>
+ * If not set, a default client is created via {@link
HttpClients#createDefault()}.
+ *
+ * <p>
+ * The transport takes ownership of the client and will close it when
{@link ApacheHc5Transport#close()} is called.
+ *
+ * @param value The client to use. Must not be <jk>null</jk>.
+ * @return This object.
+ */
+ public ApacheHc5TransportBuilder httpClient(CloseableHttpClient value) {
+ httpClient = value;
+ return this;
+ }
+
+ /**
+ * Builds and returns the {@link ApacheHc5Transport}.
+ *
+ * @return A new instance. Never <jk>null</jk>.
+ */
+ public ApacheHc5Transport build() {
+ return new ApacheHc5Transport(this);
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportProvider.java
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportProvider.java
new file mode 100644
index 0000000000..bed1d6ffe0
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportProvider.java
@@ -0,0 +1,61 @@
+/*
+ * 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 org.apache.juneau.ng.rest.client.apachehttpclient50;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransportProvider} implementation that supplies an {@link
ApacheHc5Transport}.
+ *
+ * <p>
+ * Registered via {@code
META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider} so
that
+ * {@code NgRestClient} can auto-discover Apache HttpClient 5.x when this
module is on the classpath.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client">juneau-ng
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class ApacheHc5TransportProvider implements HttpTransportProvider
{
+
+ @Override /* HttpTransportProvider */
+ public int getPriority() {
+ return 60;
+ }
+
+ @Override /* HttpTransportProvider */
+ public boolean isAvailable() {
+ try {
+
Class.forName("org.apache.hc.client5.http.impl.classic.CloseableHttpClient");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ @Override /* HttpTransportProvider */
+ public HttpTransport create() {
+ return ApacheHc5Transport.create();
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
new file mode 100644
index 0000000000..9ed6dec432
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
@@ -0,0 +1,16 @@
+# 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.
+
+org.apache.juneau.ng.rest.client.apachehttpclient50.ApacheHc5TransportProvider
diff --git a/juneau-rest/juneau-ng-rest-client-java-httpclient/pom.xml
b/juneau-rest/juneau-ng-rest-client-java-httpclient/pom.xml
new file mode 100644
index 0000000000..c2d222e942
--- /dev/null
+++ b/juneau-rest/juneau-ng-rest-client-java-httpclient/pom.xml
@@ -0,0 +1,83 @@
+<?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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest</artifactId>
+ <version>9.2.1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>juneau-ng-rest-client-java-httpclient</artifactId>
+ <name>Apache Juneau NG REST Client — JDK HttpClient Transport</name>
+ <description>JDK java.net.http.HttpClient transport adapter for the
next-generation Juneau REST client.</description>
+ <packaging>bundle</packaging>
+
+ <properties>
+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+
<supportIncrementalBuild>true</supportIncrementalBuild>
+ </configuration>
+ <executions>
+ <execution>
+ <id>bundle-manifest</id>
+ <phase>process-classes</phase>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransport.java
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransport.java
new file mode 100644
index 0000000000..b2fbdbc14b
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransport.java
@@ -0,0 +1,140 @@
+/*
+ * 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 org.apache.juneau.ng.rest.client.javahttpclient;
+
+import java.io.*;
+import java.net.http.*;
+import java.net.http.HttpRequest.*;
+import java.net.http.HttpResponse.*;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransport} implementation backed by the JDK's built-in {@link
java.net.http.HttpClient}.
+ *
+ * <p>
+ * No external dependencies are required — this transport is always available
on Java 11+.
+ * It is auto-discovered via {@link java.util.ServiceLoader} and can also be
instantiated explicitly:
+ *
+ * <p class='bjava'>
+ * <jv>transport</jv> = JavaHttpTransport.<jsm>builder</jsm>()
+ * .httpClient(HttpClient.newHttpClient())
+ * .build();
+ *
+ * <jv>client</jv> = NgRestClient.<jsm>builder</jsm>()
+ * .transport(<jv>transport</jv>)
+ * .build();
+ * </p>
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client">juneau-ng
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class JavaHttpTransport implements HttpTransport {
+
+ private final java.net.http.HttpClient httpClient;
+
+ JavaHttpTransport(JavaHttpTransportBuilder builder) {
+ this.httpClient = builder.httpClient != null ?
builder.httpClient : java.net.http.HttpClient.newHttpClient();
+ }
+
+ /**
+ * Returns a new builder for this transport.
+ *
+ * @return A new builder. Never <jk>null</jk>.
+ */
+ public static JavaHttpTransportBuilder builder() {
+ return new JavaHttpTransportBuilder();
+ }
+
+ /**
+ * Returns a new instance backed by a default {@link
java.net.http.HttpClient}.
+ *
+ * @return A new instance. Never <jk>null</jk>.
+ */
+ public static JavaHttpTransport create() {
+ return builder().build();
+ }
+
+ @Override /* HttpTransport */
+ public TransportResponse execute(TransportRequest request) throws
TransportException {
+ HttpRequest jdkRequest;
+ try {
+ jdkRequest = buildJdkRequest(request);
+ } catch (IOException e) {
+ throw new TransportException("Failed to build HTTP
request: " + e.getMessage(), e);
+ }
+ HttpResponse<InputStream> jdkResponse;
+ try {
+ jdkResponse = httpClient.send(jdkRequest,
BodyHandlers.ofInputStream());
+ } catch (IOException e) {
+ throw new TransportException("HTTP transport error: " +
e.getMessage(), e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new TransportException("HTTP request
interrupted", e);
+ }
+ return buildTransportResponse(jdkResponse);
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // Internal helpers
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ private static HttpRequest buildJdkRequest(TransportRequest request)
throws IOException {
+ var builder =
HttpRequest.newBuilder().uri(request.getUri()).method(request.getMethod(),
buildBodyPublisher(request.getBody()));
+ for (var h : request.getHeaders())
+ builder.header(h.name(), h.value());
+ return builder.build();
+ }
+
+ private static BodyPublisher buildBodyPublisher(TransportBody body)
throws IOException {
+ if (body == null)
+ return BodyPublishers.noBody();
+ // Use a pipe so body.writeTo() streams directly to the JDK
client without full in-memory buffering.
+ // The writer runs on a daemon thread; the JDK client reads
from the PipedInputStream on its own threads.
+ var in = new PipedInputStream();
+ var out = new PipedOutputStream(in);
+ var writer = new Thread(() -> {
+ try (out) {
+ body.writeTo(out);
+ } catch (IOException e) {
+ // Closing the pipe on error causes the reader
to see an IOException,
+ // which propagates to the JDK client as a send
failure.
+ }
+ }, "juneau-ng-body-writer");
+ writer.setDaemon(true);
+ writer.start();
+ return BodyPublishers.ofInputStream(() -> in);
+ }
+
+ private static TransportResponse
buildTransportResponse(HttpResponse<InputStream> jdkResponse) {
+ var builder = TransportResponse.builder()
+ .statusCode(jdkResponse.statusCode())
+ .body(jdkResponse.body());
+ jdkResponse.headers().map().forEach((name, values) ->
+ values.forEach(value -> builder.header(name, value)));
+ return builder.build();
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportBuilder.java
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportBuilder.java
new file mode 100644
index 0000000000..b3edc17f25
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportBuilder.java
@@ -0,0 +1,67 @@
+/*
+ * 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 org.apache.juneau.ng.rest.client.javahttpclient;
+
+import java.net.http.*;
+
+/**
+ * Fluent builder for {@link JavaHttpTransport}.
+ *
+ * <p>
+ * Obtain an instance via {@link JavaHttpTransport#builder()}.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client">juneau-ng
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class JavaHttpTransportBuilder {
+
+ HttpClient httpClient;
+
+ JavaHttpTransportBuilder() {}
+
+ /**
+ * Sets the underlying {@link HttpClient} to use.
+ *
+ * <p>
+ * If not set, a default client is created via {@link
HttpClient#newHttpClient()}.
+ *
+ * @param value The client to use. Must not be <jk>null</jk>.
+ * @return This object.
+ */
+ public JavaHttpTransportBuilder httpClient(HttpClient value) {
+ httpClient = value;
+ return this;
+ }
+
+ /**
+ * Builds and returns the {@link JavaHttpTransport}.
+ *
+ * @return A new instance. Never <jk>null</jk>.
+ */
+ public JavaHttpTransport build() {
+ return new JavaHttpTransport(this);
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportProvider.java
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportProvider.java
new file mode 100644
index 0000000000..af7cb421e4
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportProvider.java
@@ -0,0 +1,61 @@
+/*
+ * 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 org.apache.juneau.ng.rest.client.javahttpclient;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransportProvider} implementation that supplies a {@link
JavaHttpTransport}.
+ *
+ * <p>
+ * Registered via {@code
META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider} so
that
+ * {@code NgRestClient} can auto-discover the JDK HTTP client when no
higher-priority transport is present.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client">juneau-ng
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class JavaHttpTransportProvider implements HttpTransportProvider {
+
+ @Override /* HttpTransportProvider */
+ public int getPriority() {
+ return 80;
+ }
+
+ @Override /* HttpTransportProvider */
+ public boolean isAvailable() {
+ try {
+ Class.forName("java.net.http.HttpClient");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ @Override /* HttpTransportProvider */
+ public HttpTransport create() {
+ return JavaHttpTransport.create();
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
new file mode 100644
index 0000000000..df29856c35
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
@@ -0,0 +1,16 @@
+# 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.
+
+org.apache.juneau.ng.rest.client.javahttpclient.JavaHttpTransportProvider
diff --git a/juneau-rest/pom.xml b/juneau-rest/pom.xml
index cde05c0514..896d9e23f5 100644
--- a/juneau-rest/pom.xml
+++ b/juneau-rest/pom.xml
@@ -38,6 +38,8 @@
<module>juneau-rest-client</module>
<module>juneau-rest-mock</module>
<module>juneau-ng-rest-client-apache-httpclient-45</module>
+ <module>juneau-ng-rest-client-apache-httpclient-50</module>
+ <module>juneau-ng-rest-client-java-httpclient</module>
</modules>
<build>
diff --git a/juneau-utest/pom.xml b/juneau-utest/pom.xml
index 4d8794c9d0..3a9955ec6d 100644
--- a/juneau-utest/pom.xml
+++ b/juneau-utest/pom.xml
@@ -90,6 +90,16 @@
<artifactId>juneau-ng-rest-client-apache-httpclient-45</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+
<artifactId>juneau-ng-rest-client-apache-httpclient-50</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+
<artifactId>juneau-ng-rest-client-java-httpclient</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<!-- Force commons-codec >= 1.13 to satisfy Jena's MurmurHash3
requirement;
httpclient:4.5.14 pulls in 1.11 which pre-dates
MurmurHash3. -->
<dependency>
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/ApacheHc5Transport_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/ApacheHc5Transport_Test.java
new file mode 100644
index 0000000000..ae4b8989d0
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/ApacheHc5Transport_Test.java
@@ -0,0 +1,274 @@
+/*
+ * 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 org.apache.juneau.ng.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.*;
+
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.apachehttpclient50.*;
+import org.junit.jupiter.api.*;
+
+import com.sun.net.httpserver.*;
+
+/**
+ * Integration tests for {@link ApacheHc5Transport} against a real embedded
HTTP server.
+ */
+@SuppressWarnings("restriction")
+public class ApacheHc5Transport_Test {
+
+ private static HttpServer server;
+ private static int port;
+
+ @BeforeAll
+ static void startServer() throws IOException {
+ server = HttpServer.create(new InetSocketAddress(0), 0);
+ port = server.getAddress().getPort();
+
+ server.createContext("/hello", exchange -> {
+ var body = "Hello,
World!".getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type",
"text/plain");
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+
+ server.createContext("/echo-method", exchange -> {
+ var body =
exchange.getRequestMethod().getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type",
"text/plain");
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+
+ server.createContext("/echo-body", exchange -> {
+ var requestBody =
exchange.getRequestBody().readAllBytes();
+ exchange.getResponseHeaders().add("Content-Type",
"text/plain");
+ exchange.sendResponseHeaders(200, requestBody.length);
+ exchange.getResponseBody().write(requestBody);
+ exchange.close();
+ });
+
+ server.createContext("/echo-header", exchange -> {
+ var headerValue =
exchange.getRequestHeaders().getFirst("X-Custom");
+ var body = (headerValue != null ? headerValue :
"missing").getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type",
"text/plain");
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+
+ server.createContext("/not-found", exchange -> {
+ exchange.sendResponseHeaders(404, -1);
+ exchange.close();
+ });
+
+ server.start();
+ }
+
+ @AfterAll
+ static void stopServer() {
+ if (server != null)
+ server.stop(0);
+ }
+
+ private String rootUrl() {
+ return "http://localhost:" + port;
+ }
+
+ //
=================================================================================================================
+ // A — Basic connectivity
+ //
=================================================================================================================
+
+ @Test
+ void a01_get_basicResponse() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/hello").run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("Hello, World!",
response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void a02_get_statusCode404() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/not-found").run()) {
+ assertEquals(404, response.getStatusCode());
+ }
+ }
+ }
+
+ @Test
+ void a03_get_responseHeader() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/hello").run()) {
+ var ct =
response.getFirstHeader("Content-Type");
+ assertNotNull(ct);
+ assertTrue(ct.value().startsWith("text/plain"),
"Expected text/plain but got: " + ct.value());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // B — HTTP methods
+ //
=================================================================================================================
+
+ @Test
+ void b01_post_echosMethod() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.post("/echo-method")
+ .body(StringBody.of("", "text/plain"))
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("POST",
response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void b02_put_echosMethod() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.put("/echo-method")
+ .body(StringBody.of("", "text/plain"))
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("PUT", response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void b03_delete_echosMethod() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response =
client.delete("/echo-method").run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("DELETE",
response.getBodyAsString());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // C — Request body
+ //
=================================================================================================================
+
+ @Test
+ void c01_post_stringBody() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.post("/echo-body")
+ .body(StringBody.of("hello body",
"text/plain"))
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("hello body",
response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void c02_post_byteArrayBody() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ var bytes = "byte
content".getBytes(StandardCharsets.UTF_8);
+ try (var response = client.post("/echo-body")
+ .body(ByteArrayBody.of(bytes,
"application/octet-stream"))
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("byte content",
response.getBodyAsString());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // D — Request headers
+ //
=================================================================================================================
+
+ @Test
+ void d01_header_sentToServer() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/echo-header")
+ .header("X-Custom", "my-value")
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("my-value",
response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void d02_missingHeader_returnsDefault() throws Exception {
+ var transport = ApacheHc5Transport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/echo-header").run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("missing",
response.getBodyAsString());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // E — Builder: explicit httpClient
+ //
=================================================================================================================
+
+ @Test
+ void e01_builder_withExplicitHttpClient() throws Exception {
+ var transport = ApacheHc5Transport.builder()
+
.httpClient(org.apache.hc.client5.http.impl.classic.HttpClients.createDefault())
+ .build();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/hello").run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("Hello, World!",
response.getBodyAsString());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // F — Provider
+ //
=================================================================================================================
+
+ @Test
+ void f01_provider_isAvailable() {
+ var provider = new ApacheHc5TransportProvider();
+ assertTrue(provider.isAvailable());
+ }
+
+ @Test
+ void f02_provider_priority() {
+ var provider = new ApacheHc5TransportProvider();
+ assertEquals(60, provider.getPriority());
+ }
+
+ @Test
+ void f03_provider_create() throws Exception {
+ var provider = new ApacheHc5TransportProvider();
+ try (var transport = provider.create()) {
+ assertNotNull(transport);
+ }
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JavaHttpTransport_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JavaHttpTransport_Test.java
new file mode 100644
index 0000000000..0bebbe7bcf
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JavaHttpTransport_Test.java
@@ -0,0 +1,275 @@
+/*
+ * 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 org.apache.juneau.ng.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.net.*;
+import java.net.http.*;
+import java.nio.charset.*;
+
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.javahttpclient.*;
+import org.junit.jupiter.api.*;
+
+import com.sun.net.httpserver.*;
+
+/**
+ * Integration tests for {@link JavaHttpTransport} against a real embedded
HTTP server.
+ */
+@SuppressWarnings("restriction")
+public class JavaHttpTransport_Test {
+
+ private static HttpServer server;
+ private static int port;
+
+ @BeforeAll
+ static void startServer() throws IOException {
+ server = HttpServer.create(new InetSocketAddress(0), 0);
+ port = server.getAddress().getPort();
+
+ server.createContext("/hello", exchange -> {
+ var body = "Hello,
World!".getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type",
"text/plain");
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+
+ server.createContext("/echo-method", exchange -> {
+ var body =
exchange.getRequestMethod().getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type",
"text/plain");
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+
+ server.createContext("/echo-body", exchange -> {
+ var requestBody =
exchange.getRequestBody().readAllBytes();
+ exchange.getResponseHeaders().add("Content-Type",
"text/plain");
+ exchange.sendResponseHeaders(200, requestBody.length);
+ exchange.getResponseBody().write(requestBody);
+ exchange.close();
+ });
+
+ server.createContext("/echo-header", exchange -> {
+ var headerValue =
exchange.getRequestHeaders().getFirst("X-Custom");
+ var body = (headerValue != null ? headerValue :
"missing").getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type",
"text/plain");
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+
+ server.createContext("/not-found", exchange -> {
+ exchange.sendResponseHeaders(404, -1);
+ exchange.close();
+ });
+
+ server.start();
+ }
+
+ @AfterAll
+ static void stopServer() {
+ if (server != null)
+ server.stop(0);
+ }
+
+ private String rootUrl() {
+ return "http://localhost:" + port;
+ }
+
+ //
=================================================================================================================
+ // A — Basic connectivity
+ //
=================================================================================================================
+
+ @Test
+ void a01_get_basicResponse() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/hello").run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("Hello, World!",
response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void a02_get_statusCode404() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/not-found").run()) {
+ assertEquals(404, response.getStatusCode());
+ }
+ }
+ }
+
+ @Test
+ void a03_get_responseHeader() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/hello").run()) {
+ var ct =
response.getFirstHeader("Content-Type");
+ assertNotNull(ct);
+ assertTrue(ct.value().startsWith("text/plain"),
"Expected text/plain but got: " + ct.value());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // B — HTTP methods
+ //
=================================================================================================================
+
+ @Test
+ void b01_post_echosMethod() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.post("/echo-method")
+ .body(StringBody.of("", "text/plain"))
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("POST",
response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void b02_put_echosMethod() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.put("/echo-method")
+ .body(StringBody.of("", "text/plain"))
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("PUT", response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void b03_delete_echosMethod() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response =
client.delete("/echo-method").run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("DELETE",
response.getBodyAsString());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // C — Request body
+ //
=================================================================================================================
+
+ @Test
+ void c01_post_stringBody() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.post("/echo-body")
+ .body(StringBody.of("hello body",
"text/plain"))
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("hello body",
response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void c02_post_byteArrayBody() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ var bytes = "byte
content".getBytes(StandardCharsets.UTF_8);
+ try (var response = client.post("/echo-body")
+ .body(ByteArrayBody.of(bytes,
"application/octet-stream"))
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("byte content",
response.getBodyAsString());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // D — Request headers
+ //
=================================================================================================================
+
+ @Test
+ void d01_header_sentToServer() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/echo-header")
+ .header("X-Custom", "my-value")
+ .run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("my-value",
response.getBodyAsString());
+ }
+ }
+ }
+
+ @Test
+ void d02_missingHeader_returnsDefault() throws Exception {
+ var transport = JavaHttpTransport.create();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/echo-header").run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("missing",
response.getBodyAsString());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // E — Builder: explicit HttpClient
+ //
=================================================================================================================
+
+ @Test
+ void e01_builder_withExplicitHttpClient() throws Exception {
+ var transport = JavaHttpTransport.builder()
+ .httpClient(HttpClient.newHttpClient())
+ .build();
+ try (var client =
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+ try (var response = client.get("/hello").run()) {
+ assertEquals(200, response.getStatusCode());
+ assertEquals("Hello, World!",
response.getBodyAsString());
+ }
+ }
+ }
+
+ //
=================================================================================================================
+ // F — Provider
+ //
=================================================================================================================
+
+ @Test
+ void f01_provider_isAvailable() {
+ var provider = new JavaHttpTransportProvider();
+ assertTrue(provider.isAvailable());
+ }
+
+ @Test
+ void f02_provider_priority() {
+ var provider = new JavaHttpTransportProvider();
+ assertEquals(80, provider.getPriority());
+ }
+
+ @Test
+ void f03_provider_create() throws Exception {
+ var provider = new JavaHttpTransportProvider();
+ try (var transport = provider.create()) {
+ assertNotNull(transport);
+ }
+ }
+}
diff --git a/todo/restclient2-transport-abstraction.md
b/todo/restclient2-transport-abstraction.md
index a661fe575e..7ebef85dc7 100644
--- a/todo/restclient2-transport-abstraction.md
+++ b/todo/restclient2-transport-abstraction.md
@@ -107,7 +107,7 @@ New code is added as new packages inside **existing**
modules where possible. On
- **New package**: `org.apache.juneau.ng.http` — no `httpcore` or `httpclient`
dependency in this package
- **Contains**: JDK-native replacements for all ~186 types in
`juneau-rest-common` that currently implement Apache HttpCore interfaces
-See [ng.http Package Design](#nghttp-package-design-in-juneau-rest-common)
below.
+See ng.http Package Design below.
### 1. `juneau-rest-client` (Existing — new package added)