diff --git a/google-http-client-apache-v3/pom.xml b/google-http-client-apache-v3/pom.xml
new file mode 100644
index 000000000..4c0dbae9e
--- /dev/null
+++ b/google-http-client-apache-v3/pom.xml
@@ -0,0 +1,115 @@
+
+ 4.0.0
+
+ com.google.http-client
+ google-http-client-parent
+ 1.44.3-SNAPSHOT
+ ../pom.xml
+
+ google-http-client-apache-v3
+ 1.44.3-SNAPSHOT
+ Apache HTTP transport v3 for the Google HTTP Client Library for Java.
+
+
+
+
+ maven-javadoc-plugin
+
+
+ https://download.oracle.com/javase/7/docs/api/
+
+ ${project.name} ${project.version}
+ ${project.artifactId} ${project.version}
+
+
+
+ maven-source-plugin
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.3.0
+
+
+ add-test-source
+ generate-test-sources
+
+ add-test-source
+
+
+
+ target/generated-test-sources
+
+
+
+
+
+
+ maven-jar-plugin
+
+
+ ${project.build.outputDirectory}/META-INF/MANIFEST.MF
+
+ com.google.api.client.http.apache.v3
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ 5.1.9
+
+
+ bundle-manifest
+ process-classes
+
+ manifest
+
+
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+ 1.8
+ 1.8
+
+
+
+
+
+
+ com.google.http-client
+ google-http-client
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+
+
+ com.google.firebase
+ firebase-admin
+ 9.3.0
+
+
+ junit
+ junit
+ test
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.3.1
+
+
+ org.apache.httpcomponents.core5
+ httpcore5-h2
+ 5.2.4
+
+
+
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Request.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Request.java
new file mode 100644
index 000000000..c2b6016b8
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Request.java
@@ -0,0 +1,92 @@
+package com.google.api.client.http.apache.v3;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
+import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.util.Timeout;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+
+@SuppressWarnings("deprecation")
+final class ApacheHttp2Request extends LowLevelHttpRequest {
+ private final CloseableHttpAsyncClient httpAsyncClient;
+ private final SimpleRequestBuilder requestBuilder;
+ private SimpleHttpRequest request;
+ private final RequestConfig.Builder requestConfig;
+
+ ApacheHttp2Request(CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) {
+ this.httpAsyncClient = httpAsyncClient;
+ this.requestBuilder = requestBuilder;
+
+ this.requestConfig = RequestConfig.custom()
+ .setRedirectsEnabled(false);
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ requestBuilder.addHeader(name, value);
+ }
+
+ @Override
+ public void setTimeout(int connectionTimeout, int readTimeout) throws IOException {
+ requestConfig
+ .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout))
+ .setResponseTimeout(Timeout.ofMilliseconds(readTimeout));
+ // .setConnectionRequestTimeout(connectionTimeout)
+ // .setResponseTimeout();
+ }
+
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ // Convert StreamingContent to bytes to set request body
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ getStreamingContent().writeTo(baos);
+ byte[] bytes = baos.toByteArray();
+ requestBuilder.setBody(bytes, ContentType.parse(getContentType()));
+
+ // Set request configs
+ requestBuilder.setRequestConfig(requestConfig.build());
+
+ // Build and execute request
+ request = requestBuilder.build();
+ final CompletableFuture responseFuture = new CompletableFuture<>();
+ try {
+ httpAsyncClient.execute(
+ SimpleRequestProducer.create(request),
+ SimpleResponseConsumer.create(),
+ new FutureCallback() {
+ @Override
+ public void completed(final SimpleHttpResponse response) {
+ responseFuture.complete(response);
+ }
+
+ @Override
+ public void failed(final Exception exception) {
+ responseFuture.completeExceptionally(exception);
+ }
+
+ @Override
+ public void cancelled() {
+ responseFuture.cancel(false);
+ }
+ });
+ final SimpleHttpResponse response = responseFuture.get();
+ return new ApacheHttp2Response(request, response);
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ throw new IOException("Error making request", e);
+ }
+ }
+}
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Response.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Response.java
new file mode 100644
index 000000000..1937b69fc
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Response.java
@@ -0,0 +1,76 @@
+package com.google.api.client.http.apache.v3;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.core5.http.Header;
+
+import com.google.api.client.http.LowLevelHttpResponse;
+
+public class ApacheHttp2Response extends LowLevelHttpResponse {
+
+ private final SimpleHttpResponse response;
+ private final Header[] allHeaders;
+
+ ApacheHttp2Response(SimpleHttpRequest request, SimpleHttpResponse response) {
+ this.response = response;
+ allHeaders = response.getHeaders();
+ }
+
+ @Override
+ public int getStatusCode() {
+ return response.getCode();
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ return new ByteArrayInputStream(response.getBodyBytes());
+ }
+
+ @Override
+ public String getContentEncoding() {
+ return response.getFirstHeader("Content-Encoding").getValue();
+ }
+
+ @Override
+ public long getContentLength() {
+ return response.getBodyText().length();
+ }
+
+ @Override
+ public String getContentType() {
+ return response.getContentType().toString();
+ }
+
+ @Override
+ public String getReasonPhrase() {
+ return response.getReasonPhrase();
+ }
+
+ @Override
+ public String getStatusLine() {
+ return response.toString();
+ }
+
+ public String getHeaderValue(String name) {
+ return response.getLastHeader(name).getValue();
+ }
+
+ @Override
+ public int getHeaderCount() {
+ return allHeaders.length;
+ }
+
+ @Override
+ public String getHeaderName(int index) {
+ return allHeaders[index].getName();
+ }
+
+ @Override
+ public String getHeaderValue(int index) {
+ return allHeaders[index].getValue();
+ }
+}
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Transport.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Transport.java
new file mode 100644
index 000000000..245953ead
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Transport.java
@@ -0,0 +1,110 @@
+package com.google.api.client.http.apache.v3;
+
+import java.io.IOException;
+import java.net.ProxySelector;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.hc.client5.http.async.HttpAsyncClient;
+import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+
+import com.google.api.client.http.HttpTransport;
+
+public final class ApacheHttp2Transport extends HttpTransport{
+
+ public final CloseableHttpAsyncClient httpAsyncClient;
+
+ public ApacheHttp2Transport() {
+ this(newDefaultHttpAsyncClient(false));
+ }
+
+ public ApacheHttp2Transport(Boolean useCustom) {
+ this(newDefaultHttpAsyncClient(useCustom));
+ }
+
+ public ApacheHttp2Transport(CloseableHttpAsyncClient httpAsyncClient) {
+ this.httpAsyncClient = httpAsyncClient;
+ httpAsyncClient.start();
+ }
+
+ public static CloseableHttpAsyncClient newDefaultHttpAsyncClient(Boolean useCustom) {
+ if (useCustom) {
+ return defaultHttpAsyncClientBuilder().build();
+ }
+ return HttpAsyncClients.createHttp2System();
+ }
+
+ public static HttpAsyncClientBuilder defaultHttpAsyncClientBuilder() {
+ return HttpAsyncClientBuilder.create()
+ .setH2Config(H2Config.DEFAULT)
+ .setHttp1Config(Http1Config.DEFAULT)
+ .setIOReactorConfig(IOReactorConfig.DEFAULT)
+ .setConnectionManager(defaultAsyncClientConnectionManager())
+ .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
+ .disableRedirectHandling()
+ .disableAutomaticRetries();
+ }
+
+ public static AsyncClientConnectionManager defaultAsyncClientConnectionManager() {
+ return defaultPoolingAsyncClientConnectionManagerBuilder()
+ .build();
+ }
+
+ public static PoolingAsyncClientConnectionManagerBuilder defaultPoolingAsyncClientConnectionManagerBuilder() {
+ return PoolingAsyncClientConnectionManagerBuilder
+ .create()
+ .useSystemProperties()
+ // .setConnectionConfigResolver(null)
+ .setDefaultConnectionConfig(defaultConnectionConfig())
+ // .setTlsConfigResolver(null)
+ .setDefaultTlsConfig(defaultTlsConfig())
+ .setTlsStrategy(null)
+ .setMaxConnTotal(200)
+ .setMaxConnPerRoute(20);
+ }
+
+ public static TlsConfig defaultTlsConfig() {
+ return TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2).build();
+ }
+
+ public static ConnectionConfig defaultConnectionConfig() {
+ return ConnectionConfig.custom()
+ // .setConnectTimeout(null)
+ // .setSocketTimeout(null)
+ .setTimeToLive(-1, TimeUnit.MILLISECONDS)
+ .build();
+ }
+
+ @Override
+ public boolean supportsMethod(String method) {
+ return true;
+ }
+
+ @Override
+ protected ApacheHttp2Request buildRequest(String method, String url) {
+ SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(method).setUri(url);
+ return new ApacheHttp2Request(httpAsyncClient, requestBuilder);
+ }
+
+ @Override
+ public void shutdown() throws IOException {
+ if (httpAsyncClient instanceof CloseableHttpAsyncClient) {
+ ((CloseableHttpAsyncClient) httpAsyncClient).close();
+ }
+ }
+
+ public HttpAsyncClient getHttpClient() {
+ return httpAsyncClient;
+ }
+}
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequest.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequest.java
new file mode 100644
index 000000000..76f0ad644
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.api.client.http.apache.v3;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
+import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.http.nio.AsyncRequestProducer;
+import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer;
+import org.apache.hc.core5.http.nio.support.AbstractAsyncResponseConsumer;
+import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.http.HttpEntity;
+
+/**
+ * @author Yaniv Inbar
+ */
+final class ApacheHttpRequest extends LowLevelHttpRequest {
+ private final HttpAsyncClientBuilder httpClientBuilder;
+ private final SimpleHttpRequest request;
+
+ private RequestConfig.Builder requestConfig;
+
+ ApacheHttpRequest(HttpAsyncClientBuilder httpClientBuilder, SimpleHttpRequest request) {
+ this.httpClientBuilder = httpClientBuilder;
+ this.request = request;
+ // disable redirects as google-http-client handles redirects
+ this.requestConfig =
+ RequestConfig.custom()
+ .setRedirectsEnabled(false)
+ // TODO: enable set these somewhere down the call
+// .setNormalizeUri(false)
+// .setStaleConnectionCheckEnabled(false)
+ ;
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ request.addHeader(name, value);
+ }
+
+ @Override
+ public void setTimeout(int connectTimeout, int readTimeout) throws IOException {
+ IOReactorConfig newConfig = IOReactorConfig.custom()
+ .setSoTimeout(Timeout.ofMilliseconds(readTimeout))
+ .build();
+ requestConfig.setConnectTimeout(Timeout.ofMilliseconds(connectTimeout));
+ httpClientBuilder.setIOReactorConfig(newConfig);
+ }
+
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ ApacheHttpRequestEntityProducer entityProducer = new ApacheHttpRequestEntityProducer(this);
+
+ request.setConfig(requestConfig.build());
+ final CompletableFuture responseFuture = new CompletableFuture<>();
+ try {
+ CloseableHttpAsyncClient client = httpClientBuilder.build();
+ client.start();
+ client.execute(
+ new BasicRequestProducer(request, entityProducer),
+ SimpleResponseConsumer.create(),
+ new FutureCallback() {
+ @Override
+ public void completed(final SimpleHttpResponse response) {
+ responseFuture.complete(response);
+ }
+
+ @Override
+ public void failed(final Exception exception) {
+ responseFuture.completeExceptionally(exception);
+ }
+
+ @Override
+ public void cancelled() {
+ responseFuture.cancel(false);
+ }
+ }
+ );
+ final SimpleHttpResponse response = responseFuture.get();
+ return new ApacheHttpResponse(request, response);
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ throw new IOException("Error making request", e);
+ }
+ }
+
+}
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequestEntity.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequestEntity.java
new file mode 100644
index 000000000..bcc8d7d59
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequestEntity.java
@@ -0,0 +1,102 @@
+package com.google.api.client.http.apache.v3;
+
+
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.http.nio.DataStreamChannel;
+import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Set;
+
+class ApacheHttpRequestEntityProducer implements AsyncEntityProducer {
+ private ApacheHttpRequest request;
+
+ private ApacheHttpRequestEntityProducer() {}
+
+ public ApacheHttpRequestEntityProducer(ApacheHttpRequest request) {
+ this.request = request;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ @Override
+ public void failed(Exception e) {
+ // noop
+ }
+
+
+ @Override
+ public long getContentLength() {
+ return request.getContentLength();
+ }
+
+ @Override
+ public String getContentType() {
+ return request.getContentType();
+ }
+
+ @Override
+ public String getContentEncoding() {
+ return request.getContentEncoding();
+ }
+
+ @Override
+ public boolean isChunked() {
+ return false;
+ }
+
+ @Override
+ public Set getTrailerNames() {
+ return Collections.emptySet();
+ }
+
+ /**
+ * Borrowed from {@link BasicAsyncEntityProducer#available()}
+ * @return
+ */
+ @Override
+ public int available() {
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public void produce(DataStreamChannel dataStreamChannel) throws IOException {
+ OutputStream dataStreamOutputStream = new OutputStream() {
+ private final java.nio.ByteBuffer buffer = ByteBuffer.allocate(1000);
+
+ @Override
+ public void write(int b) throws IOException {
+ if (buffer.remaining() == 0) {
+ flush();
+ }
+ buffer.put((byte) b);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ buffer.flip();
+ dataStreamChannel.write(buffer);
+ buffer.compact();
+ }
+
+ @Override
+ public void close() throws IOException {
+ flush();
+ dataStreamChannel.endStream();
+ }
+ };
+ request.getStreamingContent().writeTo(dataStreamOutputStream);
+ dataStreamOutputStream.close();
+ }
+
+ @Override
+ public void releaseResources() {
+ // no-op
+ }
+}
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpResponse.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpResponse.java
new file mode 100644
index 000000000..bcffcc055
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpResponse.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.api.client.http.apache.v3;
+
+import com.google.api.client.http.LowLevelHttpResponse;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.message.StatusLine;
+
+final class ApacheHttpResponse extends LowLevelHttpResponse {
+
+ private final HttpRequest request;
+ private final SimpleHttpResponse response;
+ private final Header[] allHeaders;
+
+ ApacheHttpResponse(HttpRequest request, SimpleHttpResponse response) {
+ this.request = request;
+ this.response = response;
+ allHeaders = response.getHeaders();
+ }
+
+ @Override
+ public int getStatusCode() {
+ StatusLine statusLine = new StatusLine(response);
+ return statusLine == null ? 0 : statusLine.getStatusCode();
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ return new ByteArrayInputStream(response.getBodyBytes());
+ }
+
+ @Override
+ public String getContentEncoding() {
+ return response.getFirstHeader("Content-Encoding").getValue();
+ }
+
+ @Override
+ public long getContentLength() {
+ return response.getBodyText().length();
+ }
+
+ @Override
+ public String getContentType() {
+ return response.getContentType().toString();
+ }
+
+ @Override
+ public String getReasonPhrase() {
+ return response.getReasonPhrase();
+ }
+
+ @Override
+ public String getStatusLine() {
+ return new StatusLine(response).toString();
+ }
+
+ @Override
+ public int getHeaderCount() {
+ return allHeaders.length;
+ }
+
+ @Override
+ public String getHeaderName(int index) {
+ return allHeaders[index].getName();
+ }
+
+ @Override
+ public String getHeaderValue(int index) {
+ return allHeaders[index].getValue();
+ }
+}
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpTransport.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpTransport.java
new file mode 100644
index 000000000..d96d2ea3b
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpTransport.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.api.client.http.apache.v3;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.util.Beta;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+
+import java.io.IOException;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Thread-safe HTTP transport based on the Apache HTTP Client library.
+ *
+ *
Implementation is thread-safe, as long as any parameter modification to the {@link
+ * #getHttpClient() Apache HTTP Client} is only done at initialization time. For maximum efficiency,
+ * applications should use a single globally-shared instance of the HTTP transport.
+ *
+ *
Default settings are specified in {@link #newDefaultHttpClient()}. Use the {@link
+ * #ApacheHttpTransport(CloseableHttpAsyncClient)} constructor to override the Apache HTTP Client used. Please
+ * read the
+ * Apache HTTP Client connection management tutorial for more complex configuration options.
+ *
+ * @author Yaniv Inbar
+ * @since 1.30
+ */
+public final class ApacheHttpTransport extends HttpTransport {
+
+ /**
+ * Apache HTTP client.
+ */
+ private final HttpAsyncClientBuilder httpClientBuilder;
+
+ /**
+ * If the HTTP client uses mTLS channel.
+ */
+ private final boolean isMtls;
+
+ /**
+ * Constructor that uses {@link #newDefaultHttpClient()} for the Apache HTTP client.
+ *
+ * @since 1.30
+ */
+ public ApacheHttpTransport() {
+ this(newDefaultHttpClientBuilder(), false);
+ }
+
+ /**
+ * Constructor that allows an alternative Apache HTTP client to be used.
+ *
+ *
Note that in the previous version, we overrode several settings. However, we are no longer
+ * able to do so.
+ *
+ *
If you choose to provide your own Apache HttpClient implementation, be sure that
+ *
+ *
+ *
HTTP version is set to 1.1.
+ *
Redirects are disabled (google-http-client handles redirects).
+ *
Retries are disabled (google-http-client handles retries).
+ *
+ *
+ * @param httpClientBuilder Apache HTTP client to use
+ * @since 1.30
+ */
+ public ApacheHttpTransport(HttpAsyncClientBuilder httpClientBuilder) {
+ this.httpClientBuilder = httpClientBuilder;
+ this.isMtls = false;
+ }
+
+ /**
+ * {@link Beta}
+ * Constructor that allows an alternative Apache HTTP client to be used.
+ *
+ *
Note that in the previous version, we overrode several settings. However, we are no longer
+ * able to do so.
+ *
+ *
If you choose to provide your own Apache HttpClient implementation, be sure that
+ *
+ *
+ *
HTTP version is set to 1.1.
+ *
Redirects are disabled (google-http-client handles redirects).
+ *
Retries are disabled (google-http-client handles retries).
+ *
+ *
+ * @param httpClientBuilder Apache HTTP client to use
+ * @param isMtls If the HTTP client is mutual TLS
+ * @since 1.38
+ */
+ @Beta
+ public ApacheHttpTransport(HttpAsyncClientBuilder httpClientBuilder, boolean isMtls) {
+ this.httpClientBuilder = httpClientBuilder;
+ this.isMtls = isMtls;
+ }
+
+ /**
+ * Creates a new instance of the Apache HTTP client that is used by the {@link
+ * #ApacheHttpTransport()} constructor.
+ *
+ *
Settings:
+ *
+ *
+ *
The client connection manager is set to {@link PoolingHttpClientConnectionManager}.
+ *
The route planner uses {@link SystemDefaultRoutePlanner} with {@link
+ * ProxySelector#getDefault()}, which uses the proxy settings from system
+ * properties.
+ *
+ *
+ * @return new instance of the Apache HTTP client
+ * @since 1.30
+ */
+ public static CloseableHttpAsyncClient newDefaultHttpClient() {
+ return newDefaultHttpClientBuilder().build();
+ }
+
+ /**
+ * Creates a new Apache HTTP client builder that is used by the {@link #ApacheHttpTransport()}
+ * constructor.
+ *
+ *
Settings:
+ *
+ *
+ *
The client connection manager is set to {@link PoolingHttpClientConnectionManager}.
+ *
The route planner uses {@link SystemDefaultRoutePlanner} with {@link
+ * ProxySelector#getDefault()}, which uses the proxy settings from system
+ * properties.
+ *
+ *
+ * @return new instance of the Apache HTTP client
+ * @since 1.31
+ */
+ public static HttpAsyncClientBuilder newDefaultHttpClientBuilder() {
+
+ ConnectionConfig connectionConfig = ConnectionConfig.custom()
+ .setTimeToLive(-1, TimeUnit.MILLISECONDS)
+ .build();
+ PoolingAsyncClientConnectionManager connectionManager = new PoolingAsyncClientConnectionManager();
+ connectionManager.setMaxTotal(200);
+ connectionManager.setDefaultMaxPerRoute(20);
+ connectionManager.setDefaultConnectionConfig(connectionConfig);
+ connectionManager.setDefaultTlsConfig(TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2).build());
+
+
+ return HttpAsyncClientBuilder.create()
+ .setH2Config(H2Config.DEFAULT)
+ .setHttp1Config(Http1Config.DEFAULT)
+ .setIOReactorConfig(IOReactorConfig.custom().setIoThreadCount(20).build())
+ .setConnectionManager(connectionManager)
+ // socket factories are not configurable in the async client
+ //.setSSLSocketFactory(SSLConnectionSocketFactory.getSocketFactory())
+ .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
+ .disableRedirectHandling()
+ .disableAutomaticRetries();
+ }
+
+ @Override
+ public boolean supportsMethod(String method) {
+ return true;
+ }
+
+ @Override
+ protected ApacheHttpRequest buildRequest(String method, String url) {
+ SimpleHttpRequest request = new SimpleHttpRequest(method, URI.create(url));
+ return new ApacheHttpRequest(httpClientBuilder, request);
+ }
+
+ /**
+ * Shuts down the connection manager and releases allocated resources. This closes all
+ * connections, whether they are currently used or not.
+ *
+ * @since 1.30
+ */
+ @Override
+ public void shutdown() throws IOException {
+ // no-op: we create short-lived clients in the requests
+ }
+
+ /**
+ * Returns the Apache HTTP client.
+ *
+ * @since 1.30
+ */
+ public HttpAsyncClientBuilder getHttpClientBuilder() {
+ return httpClientBuilder;
+ }
+
+ /**
+ * Returns if the underlying HTTP client is mTLS.
+ */
+ @Override
+ public boolean isMtls() {
+ return isMtls;
+ }
+}
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/package-info.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/package-info.java
new file mode 100644
index 000000000..0ff3f6518
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/**
+ * HTTP Transport library for Google API's based on Apache HTTP Client version 4.5+.
+ *
+ * @since 1.30
+ * @author Yaniv Inbar
+ */
+package com.google.api.client.http.apache.v3;
diff --git a/google-http-client-apache-v3/src/main/resources/META-INF/native-image/com.google.http-client/google-http-client-apache-v2/reflect-config.json b/google-http-client-apache-v3/src/main/resources/META-INF/native-image/com.google.http-client/google-http-client-apache-v2/reflect-config.json
new file mode 100644
index 000000000..97a9fba46
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/resources/META-INF/native-image/com.google.http-client/google-http-client-apache-v2/reflect-config.json
@@ -0,0 +1,59 @@
+[
+ {
+ "name": "org.apache.commons.logging.impl.LogFactoryImpl",
+ "queryAllDeclaredConstructors": true,
+ "queryAllPublicConstructors": true,
+ "queryAllDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "org.apache.commons.logging.impl.Log4JLogger",
+ "queryAllDeclaredConstructors": true,
+ "queryAllPublicConstructors": true,
+ "queryAllDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "org.apache.commons.logging.impl.Jdk14Logger",
+ "queryAllDeclaredConstructors": true,
+ "queryAllPublicConstructors": true,
+ "queryAllDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "org.apache.commons.logging.impl.SimpleLog",
+ "queryAllDeclaredConstructors": true,
+ "queryAllPublicConstructors": true,
+ "queryAllDeclaredMethods": true,
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
+ "queryAllDeclaredConstructors": true,
+ "queryAllPublicConstructors": true,
+ "queryAllDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "org.apache.commons.logging.LogFactory",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ }
+]
\ No newline at end of file
diff --git a/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpRequestTest.java b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpRequestTest.java
new file mode 100644
index 000000000..983d74ebc
--- /dev/null
+++ b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpRequestTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.api.client.http.apache.v3;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.http.ByteArrayContent;
+import com.google.api.client.http.HttpContent;
+import com.google.api.client.http.InputStreamContent;
+import com.google.api.client.testing.http.apache.MockHttpClient;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import org.junit.Test;
+
+public class ApacheHttpRequestTest {
+
+// @Test
+// public void testContentLengthSet() throws Exception {
+// HttpExtensionMethod base = new HttpExtensionMethod("POST", "http://www.google.com");
+// ApacheHttpRequest request = new ApacheHttpRequest(new MockHttpClient(), base);
+// HttpContent content =
+// new ByteArrayContent("text/plain", "sample".getBytes(StandardCharsets.UTF_8));
+// request.setStreamingContent(content);
+// request.setContentLength(content.getLength());
+// request.execute();
+//
+// assertFalse(base.getEntity().isChunked());
+// assertEquals(6, base.getEntity().getContentLength());
+// }
+//
+// @Test
+// public void testChunked() throws Exception {
+// byte[] buf = new byte[300];
+// Arrays.fill(buf, (byte) ' ');
+// HttpExtensionMethod base = new HttpExtensionMethod("POST", "http://www.google.com");
+// ApacheHttpRequest request = new ApacheHttpRequest(new MockHttpClient(), base);
+// HttpContent content = new InputStreamContent("text/plain", new ByteArrayInputStream(buf));
+// request.setStreamingContent(content);
+// request.execute();
+//
+// assertTrue(base.getEntity().isChunked());
+// assertEquals(-1, base.getEntity().getContentLength());
+// }
+}
diff --git a/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpTransportTest.java b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpTransportTest.java
new file mode 100644
index 000000000..5f1b5ba09
--- /dev/null
+++ b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpTransportTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.api.client.http.apache.v3;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.apache.MockHttpClient;
+import com.google.api.client.util.ByteArrayStreamingContent;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests {@link ApacheHttpTransport}.
+ *
+ * @author Yaniv Inbar
+ */
+public class ApacheHttpTransportTest {
+
+ private static class MockHttpResponse extends BasicHttpResponse {
+ public MockHttpResponse() {
+ super(200, "OK");
+ }
+ }
+
+ @Test
+ public void testApacheHttpTransport() {
+ ApacheHttpTransport transport = new ApacheHttpTransport();
+ checkHttpTransport(transport);
+ assertFalse(transport.isMtls());
+ }
+
+ @Test
+ public void testApacheHttpTransportWithParam() {
+ ApacheHttpTransport transport = new ApacheHttpTransport(HttpAsyncClientBuilder.create(), true);
+ checkHttpTransport(transport);
+ assertTrue(transport.isMtls());
+ }
+
+ @Test
+ public void testNewDefaultHttpClient() {
+ HttpAsyncClientBuilder client = ApacheHttpTransport.newDefaultHttpClientBuilder();
+ checkHttpClient(client.build());
+ }
+
+ private void checkHttpTransport(ApacheHttpTransport transport) {
+ assertNotNull(transport);
+ HttpAsyncClientBuilder clientBuilder = transport.getHttpClientBuilder();
+ checkHttpClient(clientBuilder.build());
+ }
+
+ private void checkHttpClient(CloseableHttpAsyncClient client) {
+ assertNotNull(client);
+ // TODO(chingor): Is it possible to test this effectively? The newer HttpClient implementations
+ // are read-only and we're testing that we built the client with the right configuration
+ }
+
+// @Test
+// public void testRequestsWithContent() throws IOException {
+// HttpClient mockClient =
+// new MockHttpClient() {
+// @Override
+// public CloseableHttpResponse execute(HttpUriRequest request)
+// throws IOException, ClientProtocolException {
+// return new MockHttpResponse();
+// }
+// };
+// ApacheHttpTransport transport = new ApacheHttpTransport(mockClient);
+//
+// // Test GET.
+// subtestUnsupportedRequestsWithContent(
+// transport.buildRequest("GET", "http://www.test.url"), "GET");
+// // Test DELETE.
+// subtestUnsupportedRequestsWithContent(
+// transport.buildRequest("DELETE", "http://www.test.url"), "DELETE");
+// // Test HEAD.
+// subtestUnsupportedRequestsWithContent(
+// transport.buildRequest("HEAD", "http://www.test.url"), "HEAD");
+//
+// // Test PATCH.
+// execute(transport.buildRequest("PATCH", "http://www.test.url"));
+// // Test PUT.
+// execute(transport.buildRequest("PUT", "http://www.test.url"));
+// // Test POST.
+// execute(transport.buildRequest("POST", "http://www.test.url"));
+// // Test PATCH.
+// execute(transport.buildRequest("PATCH", "http://www.test.url"));
+// }
+
+// private void subtestUnsupportedRequestsWithContent(ApacheHttpRequest request, String method)
+// throws IOException {
+// try {
+// execute(request);
+// fail("expected " + IllegalStateException.class);
+// } catch (IllegalStateException e) {
+// // expected
+// assertEquals(
+// e.getMessage(),
+// "Apache HTTP client does not support " + method + " requests with content.");
+// }
+// }
+
+ private void execute(ApacheHttpRequest request) throws IOException {
+ byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8);
+ request.setStreamingContent(new ByteArrayStreamingContent(bytes));
+ request.setContentType("text/html");
+ request.setContentLength(bytes.length);
+ request.execute();
+ }
+
+// @Test
+// public void testRequestShouldNotFollowRedirects() throws IOException {
+// final AtomicInteger requestsAttempted = new AtomicInteger(0);
+// HttpRequestExecutor requestExecutor =
+// new HttpRequestExecutor() {
+//
+// @Override
+// public ClassicHttpResponse execute(
+// ClassicHttpRequest request, HttpClientConnection connection, HttpContext context)
+// throws IOException, HttpException {
+// ClassicHttpResponse response = new BasicHttpResponse(302);
+// response.addHeader("location", "https://google.com/path");
+// requestsAttempted.incrementAndGet();
+// return response;
+// }
+// };
+// CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create().setRequest(requestExecutor).build();
+// ApacheHttpTransport transport = new ApacheHttpTransport(client);
+// ApacheHttpRequest request = transport.buildRequest("GET", "https://google.com");
+// LowLevelHttpResponse response = request.execute();
+// assertEquals(1, requestsAttempted.get());
+// assertEquals(302, response.getStatusCode());
+// }
+
+// @Test
+// public void testRequestCanSetHeaders() {
+// final AtomicBoolean interceptorCalled = new AtomicBoolean(false);
+// HttpClient client =
+// HttpClients.custom()
+// .addInterceptorFirst(
+// new HttpRequestInterceptor() {
+// @Override
+// public void process(HttpRequest request, HttpContext context)
+// throws HttpException, IOException {
+// Header header = request.getFirstHeader("foo");
+// assertNotNull("Should have found header", header);
+// assertEquals("bar", header.getValue());
+// interceptorCalled.set(true);
+// throw new IOException("cancelling request");
+// }
+// })
+// .build();
+//
+// ApacheHttpTransport transport = new ApacheHttpTransport(client);
+// ApacheHttpRequest request = transport.buildRequest("GET", "https://google.com");
+// request.addHeader("foo", "bar");
+// try {
+// LowLevelHttpResponse response = request.execute();
+// fail("should not actually make the request");
+// } catch (IOException exception) {
+// assertEquals("cancelling request", exception.getMessage());
+// }
+// assertTrue("Expected to have called our test interceptor", interceptorCalled.get());
+// }
+
+// @Test(timeout = 10_000L)
+// public void testConnectTimeout() {
+// // Apache HttpClient doesn't appear to behave correctly on windows
+// assumeFalse(isWindows());
+// // TODO(chanseok): Java 17 returns an IOException (SocketException: Network is unreachable).
+// // Figure out a way to verify connection timeout works on Java 17+.
+// assumeTrue(System.getProperty("java.version").compareTo("17") < 0);
+//
+// HttpTransport httpTransport = new ApacheHttpTransport();
+// GenericUrl url = new GenericUrl("http://google.com:81");
+// try {
+// httpTransport.createRequestFactory().buildGetRequest(url).setConnectTimeout(100).execute();
+// fail("should have thrown an exception");
+// } catch (HttpHostConnectException | ConnectTimeoutException expected) {
+// // expected
+// } catch (IOException e) {
+// fail("unexpected IOException: " + e.getClass().getName() + ": " + e.getMessage());
+// }
+// }
+//
+// private static class FakeServer implements AutoCloseable {
+// private final HttpServer server;
+// private final ExecutorService executorService;
+//
+// FakeServer(HttpHandler httpHandler) throws IOException {
+// server = HttpServer.create(new InetSocketAddress(0), 0);
+// executorService = Executors.newFixedThreadPool(1);
+// server.setExecutor(executorService);
+// server.createContext("/", httpHandler);
+// server.start();
+// }
+//
+// public int getPort() {
+// return server.getAddress().getPort();
+// }
+//
+// @Override
+// public void close() {
+// server.stop(0);
+// executorService.shutdownNow();
+// }
+// }
+//
+// @Test
+// public void testNormalizedUrl() throws IOException {
+// final HttpHandler handler =
+// new HttpHandler() {
+// @Override
+// public void handle(HttpExchange httpExchange) throws IOException {
+// byte[] response = httpExchange.getRequestURI().toString().getBytes();
+// httpExchange.sendResponseHeaders(200, response.length);
+// try (OutputStream out = httpExchange.getResponseBody()) {
+// out.write(response);
+// }
+// }
+// };
+// try (FakeServer server = new FakeServer(handler)) {
+// HttpTransport transport = new ApacheHttpTransport();
+// GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
+// testUrl.setPort(server.getPort());
+// com.google.api.client.http.HttpResponse response =
+// transport.createRequestFactory().buildGetRequest(testUrl).execute();
+// assertEquals(200, response.getStatusCode());
+// assertEquals("/foo//bar", response.parseAsString());
+// }
+// }
+//
+// @Test
+// public void testReadErrorStream() throws IOException {
+// final HttpHandler handler =
+// new HttpHandler() {
+// @Override
+// public void handle(HttpExchange httpExchange) throws IOException {
+// byte[] response = "Forbidden".getBytes(StandardCharsets.UTF_8);
+// httpExchange.sendResponseHeaders(403, response.length);
+// try (OutputStream out = httpExchange.getResponseBody()) {
+// out.write(response);
+// }
+// }
+// };
+// try (FakeServer server = new FakeServer(handler)) {
+// HttpTransport transport = new ApacheHttpTransport();
+// GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
+// testUrl.setPort(server.getPort());
+// com.google.api.client.http.HttpRequest getRequest =
+// transport.createRequestFactory().buildGetRequest(testUrl);
+// getRequest.setThrowExceptionOnExecuteError(false);
+// com.google.api.client.http.HttpResponse response = getRequest.execute();
+// assertEquals(403, response.getStatusCode());
+// assertEquals("Forbidden", response.parseAsString());
+// }
+// }
+//
+// @Test
+// public void testReadErrorStream_withException() throws IOException {
+// final HttpHandler handler =
+// new HttpHandler() {
+// @Override
+// public void handle(HttpExchange httpExchange) throws IOException {
+// byte[] response = "Forbidden".getBytes(StandardCharsets.UTF_8);
+// httpExchange.sendResponseHeaders(403, response.length);
+// try (OutputStream out = httpExchange.getResponseBody()) {
+// out.write(response);
+// }
+// }
+// };
+// try (FakeServer server = new FakeServer(handler)) {
+// HttpTransport transport = new ApacheHttpTransport();
+// GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
+// testUrl.setPort(server.getPort());
+// com.google.api.client.http.HttpRequest getRequest =
+// transport.createRequestFactory().buildGetRequest(testUrl);
+// try {
+// getRequest.execute();
+// Assert.fail();
+// } catch (HttpResponseException ex) {
+// assertEquals("Forbidden", ex.getContent());
+// }
+// }
+// }
+//
+// private boolean isWindows() {
+// return System.getProperty("os.name").startsWith("Windows");
+// }
+}
diff --git a/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/FirebaseTest.java b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/FirebaseTest.java
new file mode 100644
index 000000000..6df8be22d
--- /dev/null
+++ b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/FirebaseTest.java
@@ -0,0 +1,148 @@
+package com.google.api.client.http.apache.v3;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutures;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.messaging.BatchResponse;
+import com.google.firebase.messaging.FirebaseMessaging;
+import com.google.firebase.messaging.FirebaseMessagingException;
+import com.google.firebase.messaging.Message;
+import org.junit.Test;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+public class FirebaseTest {
+ public static FirebaseApp setup_admin_apache_http2_v3() throws IOException {
+ FileInputStream serviceAccount = new FileInputStream("src/main/resources/cert.json");
+
+ FirebaseOptions options = FirebaseOptions.builder()
+ .setCredentials(GoogleCredentials.fromStream(serviceAccount))
+ .setProjectId("spring-autoconf-test")
+ .setHttpTransport(new ApacheHttp2Transport(true))
+ .build();
+
+ return FirebaseApp.initializeApp(options);
+ }
+
+ public static FirebaseApp setup_admin_apache_http_v3() throws IOException {
+ FileInputStream serviceAccount = new FileInputStream("src/main/resources/cert.json");
+
+ FirebaseOptions options = FirebaseOptions.builder()
+ .setCredentials(GoogleCredentials.fromStream(serviceAccount))
+ .setProjectId("spring-autoconf-test")
+ .setHttpTransport(new ApacheHttpTransport())
+ .build();
+
+ return FirebaseApp.initializeApp(options);
+ }
+
+ public static List get_messages(int message_count) {
+ List messages = new ArrayList<>(message_count);
+ for (int i = 0; i < message_count; i++) {
+ Message message = Message.builder()
+ .setTopic(String.format("foo-bar-%d", i % 10))
+ .build();
+ messages.add(message);
+ }
+ return messages;
+ }
+
+ public static void benchmark_send_each(List messages, int numRequests, FirebaseApp app) throws FileNotFoundException, IOException, FirebaseMessagingException {
+ long startTime = System.currentTimeMillis();
+ for (int i = 0; i < numRequests; i++) {
+ BatchResponse response = FirebaseMessaging.getInstance(app).sendEach(messages, true);
+ System.out.println("Dry Run Response: " + response.getSuccessCount());
+ }
+
+ long endTime = System.currentTimeMillis();
+ long totalTime = endTime - startTime;
+ double averageTime = (double) totalTime / numRequests;
+
+ System.out.println("Total time: " + totalTime + " ms");
+ System.out.println("Average time per request: " + averageTime + " ms");
+ }
+
+
+ public static void benchmark_send_each_async(List messages, int numRequests, FirebaseApp app) throws FileNotFoundException, IOException, FirebaseMessagingException {
+ long startTime = System.currentTimeMillis();
+ List> responseFutures = new ArrayList<>();
+
+ // Make request futures
+ for (int i = 0; i < numRequests; i++) {
+ responseFutures.add(FirebaseMessaging.getInstance(app).sendEachAsync(messages, true));
+ }
+
+ // Resolve All
+ try {
+ List responses = ApiFutures.allAsList(responseFutures).get();
+ for (BatchResponse batchResponse : responses) {
+ System.out.println("Dry Run Response: " + batchResponse.getSuccessCount());
+ }
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ throw new IOException("Error making request", e);
+ }
+
+ long endTime = System.currentTimeMillis();
+ long totalTime = endTime - startTime;
+ double averageTime = (double) totalTime / numRequests;
+
+ System.out.println("Total time: " + totalTime + " ms");
+ System.out.println("Average time per request: " + averageTime + " ms");
+ }
+
+ @SuppressWarnings("deprecation")
+ public static void benchmark_send_all(List messages, int numRequests, FirebaseApp app) throws FileNotFoundException, IOException, FirebaseMessagingException {
+ System.out.println("\nsendAll()");
+ for (int i = 0; i < numRequests; i++) {
+ FirebaseMessaging.getInstance().sendEach(messages, true);
+ }
+
+ long startTime = System.currentTimeMillis();
+ for (int i = 0; i < numRequests; i++) {
+ BatchResponse response = FirebaseMessaging.getInstance(app).sendAll(messages, true);
+ System.out.println("Dry Run Response: " + response.getSuccessCount());
+ }
+
+ long endTime = System.currentTimeMillis();
+ long totalTime = endTime - startTime;
+ double averageTime = (double) totalTime / numRequests;
+
+ System.out.println("Total time: " + totalTime + " ms");
+ System.out.println("Average time per request: " + averageTime + " ms");
+
+ // app.delete();
+ }
+
+ @Test
+ public void testFireBase() throws FirebaseMessagingException, IOException, InterruptedException {
+
+
+ List messages = get_messages(99);
+ int numRequests = 50; // Number of time to loop
+ FirebaseApp app;
+
+ System.out.println("Start");
+ app = setup_admin_apache_http2_v3();
+// app = setup_admin_apache_http_v3();
+ benchmark_send_each(messages, numRequests, app);
+ benchmark_send_each_async(messages, numRequests, app);
+ benchmark_send_all(messages, numRequests, app);
+
+ System.out.println("Sleep");
+ TimeUnit.SECONDS.sleep(5);
+ System.out.println("Awake");
+
+ app.delete();
+
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 258e85db2..4f013a2e5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -289,8 +289,8 @@
maven-compiler-plugin3.13.0
- 1.7
- 1.7
+ 1.8
+ 1.8
@@ -431,7 +431,7 @@
maven-javadoc-pluginnone
- 7
+ 8
diff --git a/versions.txt b/versions.txt
index 963efeb8d..2f22e3a1c 100644
--- a/versions.txt
+++ b/versions.txt
@@ -7,6 +7,7 @@ google-http-client-parent:1.44.2:1.44.3-SNAPSHOT
google-http-client-android:1.44.2:1.44.3-SNAPSHOT
google-http-client-android-test:1.44.2:1.44.3-SNAPSHOT
google-http-client-apache-v2:1.44.2:1.44.3-SNAPSHOT
+google-http-client-apache-v3:1.44.2:1.44.3-SNAPSHOT
google-http-client-appengine:1.44.2:1.44.3-SNAPSHOT
google-http-client-assembly:1.44.2:1.44.3-SNAPSHOT
google-http-client-findbugs:1.44.2:1.44.3-SNAPSHOT