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 c2711b80d5 Next Generation RestClient
c2711b80d5 is described below
commit c2711b80d52b47e9735710557d3864e05da6d365
Author: James Bognar <[email protected]>
AuthorDate: Thu Apr 9 16:05:05 2026 -0400
Next Generation RestClient
---
juneau-rest/juneau-ng-rest-client-jetty/pom.xml | 88 +++++++
.../ng/rest/client/jetty/JettyHttpTransport.java | 161 ++++++++++++
.../client/jetty/JettyHttpTransportBuilder.java | 89 +++++++
.../client/jetty/JettyHttpTransportProvider.java | 65 +++++
...che.juneau.ng.rest.client.HttpTransportProvider | 16 ++
juneau-rest/juneau-ng-rest-client-okhttp/pom.xml | 88 +++++++
.../ng/rest/client/okhttp/OkHttpTransport.java | 164 ++++++++++++
.../rest/client/okhttp/OkHttpTransportBuilder.java | 72 ++++++
.../client/okhttp/OkHttpTransportProvider.java | 61 +++++
...che.juneau.ng.rest.client.HttpTransportProvider | 16 ++
juneau-rest/pom.xml | 2 +
juneau-utest/pom.xml | 10 +
.../juneau/ng/rest/JettyHttpTransport_Test.java | 275 ++++++++++++++++++++
.../juneau/ng/rest/OkHttpTransport_Test.java | 276 +++++++++++++++++++++
14 files changed, 1383 insertions(+)
diff --git a/juneau-rest/juneau-ng-rest-client-jetty/pom.xml
b/juneau-rest/juneau-ng-rest-client-jetty/pom.xml
new file mode 100644
index 0000000000..9f39ae0c73
--- /dev/null
+++ b/juneau-rest/juneau-ng-rest-client-jetty/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-jetty</artifactId>
+ <name>Apache Juneau NG REST Client — Jetty HttpClient Transport</name>
+ <description>Jetty 12 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>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-client</artifactId>
+ <version>${jetty.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-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransport.java
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransport.java
new file mode 100644
index 0000000000..543feeb073
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransport.java
@@ -0,0 +1,161 @@
+/*
+ * 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.jetty;
+
+import java.io.*;
+import java.util.concurrent.*;
+
+import org.eclipse.jetty.client.*;
+import org.eclipse.jetty.http.*;
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransport} implementation backed by Jetty 12 {@link HttpClient}.
+ *
+ * <p>
+ * This transport is auto-discovered via {@link java.util.ServiceLoader} when
+ * {@code org.eclipse.jetty:jetty-client} is on the classpath. You can also
instantiate it explicitly:
+ *
+ * <p class='bjava'>
+ * <jv>transport</jv> = JettyHttpTransport.<jsm>builder</jsm>()
+ * .httpClient(<jk>new</jk> HttpClient())
+ * .build();
+ *
+ * <jv>client</jv> = NgRestClient.<jsm>builder</jsm>()
+ * .transport(<jv>transport</jv>)
+ * .build();
+ * </p>
+ *
+ * <p>
+ * The Jetty {@link HttpClient} is started automatically when the transport is
created and stopped when
+ * {@link #close()} is called. The request body is streamed via {@link
OutputStreamRequestContent} using
+ * a background thread, avoiding full in-memory buffering. The response
header timeout defaults to 30 seconds
+ * and is configurable via {@link
JettyHttpTransportBuilder#responseTimeoutMs(long)}.
+ *
+ * <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 JettyHttpTransport implements HttpTransport {
+
+ private final HttpClient httpClient;
+ private final long responseTimeoutMs;
+
+ JettyHttpTransport(JettyHttpTransportBuilder builder) throws Exception {
+ this.httpClient = builder.httpClient != null ?
builder.httpClient : new HttpClient();
+ this.responseTimeoutMs = builder.responseTimeoutMs;
+ if (!this.httpClient.isStarted())
+ this.httpClient.start();
+ }
+
+ /**
+ * Returns a new builder for this transport.
+ *
+ * @return A new builder. Never <jk>null</jk>.
+ */
+ public static JettyHttpTransportBuilder builder() {
+ return new JettyHttpTransportBuilder();
+ }
+
+ /**
+ * Returns a new instance backed by a default {@link HttpClient}.
+ *
+ * @return A new instance. Never <jk>null</jk>.
+ * @throws Exception If the Jetty {@link HttpClient} cannot be started.
+ */
+ public static JettyHttpTransport create() throws Exception {
+ return builder().build();
+ }
+
+ @Override /* HttpTransport */
+ public TransportResponse execute(TransportRequest request) throws
TransportException {
+ var jettyRequest = buildJettyRequest(request);
+ var listener = new InputStreamResponseListener();
+ jettyRequest.send(listener);
+ Response jettyResponse;
+ try {
+ jettyResponse = responseTimeoutMs > 0
+ ? listener.get(responseTimeoutMs,
TimeUnit.MILLISECONDS)
+ : listener.get(Long.MAX_VALUE,
TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new TransportException("HTTP request
interrupted", e);
+ } catch (TimeoutException e) {
+ throw new TransportException("HTTP request timed out",
e);
+ } catch (ExecutionException e) {
+ throw new TransportException("HTTP transport error: " +
e.getCause().getMessage(), e.getCause());
+ }
+ return buildTransportResponse(jettyResponse,
listener.getInputStream());
+ }
+
+ @Override /* Closeable */
+ public void close() throws IOException {
+ try {
+ httpClient.stop();
+ } catch (Exception e) {
+ throw new IOException("Failed to stop Jetty HttpClient:
" + e.getMessage(), e);
+ }
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // Internal helpers
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ private Request buildJettyRequest(TransportRequest request) {
+ var jettyRequest =
httpClient.newRequest(request.getUri()).method(request.getMethod());
+ for (var h : request.getHeaders())
+ jettyRequest.headers(fields -> fields.add(h.name(),
h.value()));
+ var body = request.getBody();
+ if (body != null)
+ jettyRequest.body(buildRequestContent(body));
+ return jettyRequest;
+ }
+
+ private static OutputStreamRequestContent
buildRequestContent(TransportBody body) {
+ var ct = body.getContentType();
+ var contentType = ct != null ? ct : "application/octet-stream";
+ var content = new OutputStreamRequestContent(contentType);
+ // Write body to the content's OutputStream on a background
thread so Jetty can stream it concurrently.
+ var writer = new Thread(() -> {
+ try (var out = content.getOutputStream()) {
+ body.writeTo(out);
+ } catch (IOException e) {
+ // Closing the stream on error signals Jetty to
abort the request.
+ }
+ }, "juneau-ng-body-writer");
+ writer.setDaemon(true);
+ writer.start();
+ return content;
+ }
+
+ private static TransportResponse buildTransportResponse(Response
jettyResponse, InputStream bodyStream) {
+ var builder = TransportResponse.builder()
+ .statusCode(jettyResponse.getStatus())
+ .reasonPhrase(jettyResponse.getReason())
+ .body(bodyStream);
+ jettyResponse.getHeaders().forEach(field ->
builder.header(field.getName(), field.getValue()));
+ return builder.build();
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportBuilder.java
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportBuilder.java
new file mode 100644
index 0000000000..18a2adbc7e
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportBuilder.java
@@ -0,0 +1,89 @@
+/*
+ * 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.jetty;
+
+import org.eclipse.jetty.client.*;
+
+/**
+ * Fluent builder for {@link JettyHttpTransport}.
+ *
+ * <p>
+ * Obtain an instance via {@link JettyHttpTransport#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 JettyHttpTransportBuilder {
+
+ HttpClient httpClient;
+ long responseTimeoutMs = 30_000;
+
+ JettyHttpTransportBuilder() {}
+
+ /**
+ * Sets the underlying {@link HttpClient} to use.
+ *
+ * <p>
+ * If not set, a default client is created via {@code new HttpClient()}.
+ *
+ * <p>
+ * If the provided client has not been started, {@link
JettyHttpTransport} will start it automatically.
+ * When {@link JettyHttpTransport#close()} is called, the client will
be stopped.
+ *
+ * @param value The client to use. Must not be <jk>null</jk>.
+ * @return This object.
+ */
+ public JettyHttpTransportBuilder httpClient(HttpClient value) {
+ httpClient = value;
+ return this;
+ }
+
+ /**
+ * Sets the maximum time to wait for the response headers to arrive
after the request is sent.
+ *
+ * <p>
+ * This timeout covers the time from when the request is fully sent
until the first response line and headers
+ * are received. It does not limit how long the response body may take
to stream.
+ * Defaults to {@code 30_000} ms (30 seconds).
+ *
+ * @param value The timeout in milliseconds. Use {@code 0} or a
negative value to wait indefinitely.
+ * @return This object.
+ */
+ public JettyHttpTransportBuilder responseTimeoutMs(long value) {
+ responseTimeoutMs = value;
+ return this;
+ }
+
+ /**
+ * Builds and returns the {@link JettyHttpTransport}.
+ *
+ * @return A new instance. Never <jk>null</jk>.
+ * @throws Exception If the Jetty {@link HttpClient} cannot be started.
+ */
+ public JettyHttpTransport build() throws Exception {
+ return new JettyHttpTransport(this);
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportProvider.java
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportProvider.java
new file mode 100644
index 0000000000..75c85a9732
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportProvider.java
@@ -0,0 +1,65 @@
+/*
+ * 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.jetty;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransportProvider} implementation that supplies a {@link
JettyHttpTransport}.
+ *
+ * <p>
+ * Registered via {@code
META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider} so
that
+ * {@code NgRestClient} can auto-discover Jetty 12 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 JettyHttpTransportProvider implements HttpTransportProvider
{
+
+ @Override /* HttpTransportProvider */
+ public int getPriority() {
+ return 75;
+ }
+
+ @Override /* HttpTransportProvider */
+ public boolean isAvailable() {
+ try {
+ Class.forName("org.eclipse.jetty.client.HttpClient");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ @Override /* HttpTransportProvider */
+ public HttpTransport create() {
+ try {
+ return JettyHttpTransport.create();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create
JettyHttpTransport: " + e.getMessage(), e);
+ }
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-jetty/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
new file mode 100644
index 0000000000..4e96a7d483
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-jetty/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.jetty.JettyHttpTransportProvider
diff --git a/juneau-rest/juneau-ng-rest-client-okhttp/pom.xml
b/juneau-rest/juneau-ng-rest-client-okhttp/pom.xml
new file mode 100644
index 0000000000..45940dd6e5
--- /dev/null
+++ b/juneau-rest/juneau-ng-rest-client-okhttp/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-okhttp</artifactId>
+ <name>Apache Juneau NG REST Client — OkHttp Transport</name>
+ <description>OkHttp 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>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp-jvm</artifactId>
+ <version>5.2.0</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-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransport.java
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransport.java
new file mode 100644
index 0000000000..e1750e0650
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransport.java
@@ -0,0 +1,164 @@
+/*
+ * 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.okhttp;
+
+import java.io.*;
+
+import okhttp3.*;
+import okio.*;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransport} implementation backed by OkHttp 5.x.
+ *
+ * <p>
+ * This transport is auto-discovered via {@link java.util.ServiceLoader} when
+ * {@code com.squareup.okhttp3:okhttp-jvm} is on the classpath. You can also
instantiate it explicitly:
+ *
+ * <p class='bjava'>
+ * <jv>transport</jv> = OkHttpTransport.<jsm>builder</jsm>()
+ * .httpClient(<jk>new</jk> OkHttpClient())
+ * .build();
+ *
+ * <jv>client</jv> = NgRestClient.<jsm>builder</jsm>()
+ * .transport(<jv>transport</jv>)
+ * .build();
+ * </p>
+ *
+ * <p>
+ * OkHttp's {@link RequestBody#writeTo(BufferedSink)} streams the request body
directly to the socket
+ * without in-memory buffering for bodies with a known content length.
+ *
+ * <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 OkHttpTransport implements HttpTransport {
+
+ private final OkHttpClient httpClient;
+
+ OkHttpTransport(OkHttpTransportBuilder builder) {
+ this.httpClient = builder.httpClient != null ?
builder.httpClient : new OkHttpClient();
+ }
+
+ /**
+ * Returns a new builder for this transport.
+ *
+ * @return A new builder. Never <jk>null</jk>.
+ */
+ public static OkHttpTransportBuilder builder() {
+ return new OkHttpTransportBuilder();
+ }
+
+ /**
+ * Returns a new instance backed by a default {@link OkHttpClient}.
+ *
+ * @return A new instance. Never <jk>null</jk>.
+ */
+ public static OkHttpTransport create() {
+ return builder().build();
+ }
+
+ @Override /* HttpTransport */
+ public TransportResponse execute(TransportRequest request) throws
TransportException {
+ var okRequest = buildOkRequest(request);
+ Response okResponse;
+ try {
+ okResponse = httpClient.newCall(okRequest).execute();
+ } catch (IOException e) {
+ throw new TransportException("HTTP transport error: " +
e.getMessage(), e);
+ }
+ return buildTransportResponse(okResponse);
+ }
+
+ @Override /* Closeable */
+ public void close() {
+ httpClient.dispatcher().executorService().shutdown();
+ httpClient.connectionPool().evictAll();
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // Internal helpers
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ private static Request buildOkRequest(TransportRequest request) throws
TransportException {
+ var builder = new
Request.Builder().url(request.getUri().toString());
+ for (var h : request.getHeaders())
+ builder.addHeader(h.name(), h.value());
+ builder.method(request.getMethod(),
buildOkBody(request.getBody(), request.getMethod()));
+ return builder.build();
+ }
+
+ private static RequestBody buildOkBody(TransportBody body, String
method) {
+ if (body == null) {
+ // OkHttp requires a non-null body for POST/PUT/PATCH
even if empty
+ return requiresBody(method) ? RequestBody.create(new
byte[0], null) : null;
+ }
+ var ct = body.getContentType();
+ var mediaType = ct != null ? MediaType.parse(ct) : null;
+ return new RequestBody() {
+ @Override
+ public MediaType contentType() {
+ return mediaType;
+ }
+
+ @Override
+ public long contentLength() {
+ return body.getContentLength();
+ }
+
+ @Override
+ public void writeTo(BufferedSink sink) throws
IOException {
+ body.writeTo(sink.outputStream());
+ }
+ };
+ }
+
+ private static boolean requiresBody(String method) {
+ return "POST".equalsIgnoreCase(method) ||
"PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method);
+ }
+
+ @SuppressWarnings({
+ "resource" // okResponse is closed via
TransportResponse.closeCallback
+ })
+ private static TransportResponse buildTransportResponse(Response
okResponse) throws TransportException {
+ var builder = TransportResponse.builder()
+ .statusCode(okResponse.code())
+ .reasonPhrase(okResponse.message())
+ .closeCallback(okResponse);
+ for (var i = 0; i < okResponse.headers().size(); i++)
+ builder.header(okResponse.headers().name(i),
okResponse.headers().value(i));
+ var responseBody = okResponse.body();
+ if (responseBody != null) {
+ try {
+ builder.body(responseBody.byteStream());
+ } catch (Exception e) {
+ throw new TransportException("Failed to read
response body: " + e.getMessage(), e);
+ }
+ }
+ return builder.build();
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportBuilder.java
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportBuilder.java
new file mode 100644
index 0000000000..dcc79d27ae
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportBuilder.java
@@ -0,0 +1,72 @@
+/*
+ * 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.okhttp;
+
+import okhttp3.*;
+
+/**
+ * Fluent builder for {@link OkHttpTransport}.
+ *
+ * <p>
+ * Obtain an instance via {@link OkHttpTransport#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 OkHttpTransportBuilder {
+
+ OkHttpClient httpClient;
+
+ OkHttpTransportBuilder() {}
+
+ /**
+ * Sets the underlying {@link OkHttpClient} to use.
+ *
+ * <p>
+ * If not set, a default client is created via {@code new
OkHttpClient()}.
+ *
+ * <p>
+ * OkHttp recommends sharing a single {@link OkHttpClient} instance
across the application.
+ * When sharing, the caller is responsible for lifecycle management;
{@link OkHttpTransport#close()}
+ * will still attempt to drain the client's connection pool and
executor.
+ *
+ * @param value The client to use. Must not be <jk>null</jk>.
+ * @return This object.
+ */
+ public OkHttpTransportBuilder httpClient(OkHttpClient value) {
+ httpClient = value;
+ return this;
+ }
+
+ /**
+ * Builds and returns the {@link OkHttpTransport}.
+ *
+ * @return A new instance. Never <jk>null</jk>.
+ */
+ public OkHttpTransport build() {
+ return new OkHttpTransport(this);
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportProvider.java
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportProvider.java
new file mode 100644
index 0000000000..faaba748c1
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportProvider.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.okhttp;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransportProvider} implementation that supplies an {@link
OkHttpTransport}.
+ *
+ * <p>
+ * Registered via {@code
META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider} so
that
+ * {@code NgRestClient} can auto-discover OkHttp 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 OkHttpTransportProvider implements HttpTransportProvider {
+
+ @Override /* HttpTransportProvider */
+ public int getPriority() {
+ return 70;
+ }
+
+ @Override /* HttpTransportProvider */
+ public boolean isAvailable() {
+ try {
+ Class.forName("okhttp3.OkHttpClient");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ @Override /* HttpTransportProvider */
+ public HttpTransport create() {
+ return OkHttpTransport.create();
+ }
+}
diff --git
a/juneau-rest/juneau-ng-rest-client-okhttp/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
new file mode 100644
index 0000000000..aba18514eb
--- /dev/null
+++
b/juneau-rest/juneau-ng-rest-client-okhttp/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.okhttp.OkHttpTransportProvider
diff --git a/juneau-rest/pom.xml b/juneau-rest/pom.xml
index 896d9e23f5..1039fd7138 100644
--- a/juneau-rest/pom.xml
+++ b/juneau-rest/pom.xml
@@ -40,6 +40,8 @@
<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>
+ <module>juneau-ng-rest-client-okhttp</module>
+ <module>juneau-ng-rest-client-jetty</module>
</modules>
<build>
diff --git a/juneau-utest/pom.xml b/juneau-utest/pom.xml
index 3a9955ec6d..28e4f6594d 100644
--- a/juneau-utest/pom.xml
+++ b/juneau-utest/pom.xml
@@ -100,6 +100,16 @@
<artifactId>juneau-ng-rest-client-java-httpclient</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-ng-rest-client-okhttp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-ng-rest-client-jetty</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/JettyHttpTransport_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JettyHttpTransport_Test.java
new file mode 100644
index 0000000000..a752c9118e
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JettyHttpTransport_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.nio.charset.*;
+
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.jetty.*;
+import org.eclipse.jetty.client.*;
+import org.junit.jupiter.api.*;
+
+import com.sun.net.httpserver.*;
+
+/**
+ * Integration tests for {@link JettyHttpTransport} against a real embedded
HTTP server.
+ */
+@SuppressWarnings("restriction")
+public class JettyHttpTransport_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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.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 = JettyHttpTransport.builder()
+ .httpClient(new HttpClient())
+ .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 JettyHttpTransportProvider();
+ assertTrue(provider.isAvailable());
+ }
+
+ @Test
+ void f02_provider_priority() {
+ var provider = new JettyHttpTransportProvider();
+ assertEquals(75, provider.getPriority());
+ }
+
+ @Test
+ void f03_provider_create() throws Exception {
+ var provider = new JettyHttpTransportProvider();
+ try (var transport = provider.create()) {
+ assertNotNull(transport);
+ }
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/OkHttpTransport_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/OkHttpTransport_Test.java
new file mode 100644
index 0000000000..0f96a22c71
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/OkHttpTransport_Test.java
@@ -0,0 +1,276 @@
+/*
+ * 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 okhttp3.*;
+
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.okhttp.*;
+import org.junit.jupiter.api.*;
+
+import com.sun.net.httpserver.*;
+
+/**
+ * Integration tests for {@link OkHttpTransport} against a real embedded HTTP
server.
+ */
+@SuppressWarnings("restriction")
+public class OkHttpTransport_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 = OkHttpTransport.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 = OkHttpTransport.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 = OkHttpTransport.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 = OkHttpTransport.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 = OkHttpTransport.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 = OkHttpTransport.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 = OkHttpTransport.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 = OkHttpTransport.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 = OkHttpTransport.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 = OkHttpTransport.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 OkHttpClient
+ //
=================================================================================================================
+
+ @Test
+ void e01_builder_withExplicitHttpClient() throws Exception {
+ var transport = OkHttpTransport.builder()
+ .httpClient(new OkHttpClient())
+ .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 OkHttpTransportProvider();
+ assertTrue(provider.isAvailable());
+ }
+
+ @Test
+ void f02_provider_priority() {
+ var provider = new OkHttpTransportProvider();
+ assertEquals(70, provider.getPriority());
+ }
+
+ @Test
+ void f03_provider_create() throws Exception {
+ var provider = new OkHttpTransportProvider();
+ try (var transport = provider.create()) {
+ assertNotNull(transport);
+ }
+ }
+}