diff --git a/.github/workflows/ci-java7.yaml b/.github/workflows/ci-java7.yaml
index 2c8257d45..e2ce98b22 100644
--- a/.github/workflows/ci-java7.yaml
+++ b/.github/workflows/ci-java7.yaml
@@ -55,7 +55,7 @@ jobs:
# dailymotion-simple-cmdline-sample and google-http-client-assembly depend on
# google-http-client-jackson2
mvn --batch-mode --show-version -ntp test \
- --projects '!google-http-client-jackson2,!google-http-client-appengine,!samples/dailymotion-simple-cmdline-sample,!google-http-client-assembly' \
+ --projects '!google-http-client-jackson2,!google-http-client-appengine,!samples/dailymotion-simple-cmdline-sample,!google-http-client-assembly,!google-http-client-apache-v5' \
-Dclirr.skip=true -Denforcer.skip=true -Dmaven.javadoc.skip=true \
-Dgcloud.download.skip=true -T 1C \
-Dproject.surefire.version=2.22.2 \
diff --git a/docs/http-transport.md b/docs/http-transport.md
index 0167db4ac..b6d94cce2 100644
--- a/docs/http-transport.md
+++ b/docs/http-transport.md
@@ -19,8 +19,12 @@ There are three built-in low-level HTTP transports:
1. [`NetHttpTransport`][net-http-transport]: based on [`HttpURLConnection`][http-url-connection]
that is found in all Java SDKs, and thus usually the simplest choice.
+1. [`Apache5HttpTransport`][apache-http-transport]: based on the popular
+ [Apache 5.x HttpClient][apache5-http-client] that allows for more customization.
1. [`ApacheHttpTransport`][apache-http-transport]: based on the popular
-[Apache HttpClient][apache-http-client] that allows for more customization.
+[Apache 4.x HttpClient][apache-http-client] that allows for more customization. Note that this transport implementation
+relies on [Apache 4.x HttpCore][apache-http-core] which has reached end of life. It is recommended to use
+[`Apache5HttpTransport`][apache-http-transport] instead.
1. [`UrlFetchTransport`][url-fetch-transport]: based on the [URL Fetch Java API][url-fetch] in the
Google App Engine SDK.
@@ -124,7 +128,10 @@ HttpRequestFactory requestFactory = transport.createRequestFactory(new MyInitial
[net-http-transport]: https://googleapis.dev/java/google-http-client/latest/index.html?com/google/api/client/http/javanet/NetHttpTransport.html
[http-url-connection]: http://docs.oracle.com/javase/7/docs/api/java/net/HttpURLConnection.html
[apache-http-transport]: https://googleapis.dev/java/google-http-client/latest/index.html?com/google/api/client/http/apache/v2/ApacheHttpTransport.html
-[apache-http-client]: http://hc.apache.org/httpcomponents-client-ga/index.html
+[apache5-http-transport]: https://github.com/googleapis/google-http-java-client/blob/de8743587d1415e8a6046096ac1fc0a5e81490c3/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java
+[apache-http-client]: https://hc.apache.org/httpcomponents-client-4.5.x/index.html
+[apache-http-core]: https://hc.apache.org/httpcomponents-core-4.4.x/index.html
+[apache5-http-client]: https://hc.apache.org/httpcomponents-client-5.3.x/index.html
[url-fetch-transport]: https://googleapis.dev/java/google-http-client/latest/index.html?com/google/api/client/extensions/appengine/http/UrlFetchTransport.html
[url-fetch]: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/urlfetch/package-summary
[logger]: https://docs.oracle.com/javase/7/docs/api/java/util/logging/Logger.html
diff --git a/google-http-client-apache-v5/pom.xml b/google-http-client-apache-v5/pom.xml
new file mode 100644
index 000000000..a67897975
--- /dev/null
+++ b/google-http-client-apache-v5/pom.xml
@@ -0,0 +1,116 @@
+
+ 4.0.0
+
+ com.google.http-client
+ google-http-client-parent
+ 1.44.3-SNAPSHOT
+ ../pom.xml
+
+ google-http-client-apache-v5
+ 1.44.3-SNAPSHOT
+ Apache HTTP transport v5 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
+
+
+
+
+
+
+
+
+
+
+ maven-jar-plugin
+
+
+ ${project.build.outputDirectory}/META-INF/MANIFEST.MF
+
+ com.google.api.client.http.apache.v5
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ 5.1.9
+
+
+ bundle-manifest
+ process-classes
+
+ manifest
+
+
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+
+ 1.8
+
+
+
+
+
+
+ com.google.http-client
+ google-http-client
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+
+
+ com.google.guava
+ guava
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+
+
+ junit
+ junit
+ test
+
+
+
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ContentEntity.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ContentEntity.java
new file mode 100644
index 000000000..4a5ab84e6
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ContentEntity.java
@@ -0,0 +1,79 @@
+/*
+ * 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.v5;
+
+import com.google.api.client.util.Preconditions;
+import com.google.api.client.util.StreamingContent;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
+
+/**
+ * Translation class to make google-http-client entity conform with Apache 5.x {@link
+ * AbstractHttpEntity}
+ */
+final class Apache5ContentEntity extends AbstractHttpEntity {
+
+ /** Content length or less than zero if not known. */
+ private final long contentLength;
+
+ /** Streaming content. */
+ private final StreamingContent streamingContent;
+
+ /**
+ * @param contentLength content length or less than zero if not known
+ * @param streamingContent streaming content
+ */
+ Apache5ContentEntity(
+ long contentLength,
+ StreamingContent streamingContent,
+ String contentType,
+ String contentEncoding) {
+ super(contentType, contentEncoding, contentLength == -1);
+ this.contentLength = contentLength;
+ this.streamingContent = Preconditions.checkNotNull(streamingContent);
+ }
+
+ @Override
+ public InputStream getContent() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getContentLength() {
+ return contentLength;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ @Override
+ public boolean isStreaming() {
+ return true;
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ if (contentLength != 0) {
+ streamingContent.writeTo(out);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {}
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpRequest.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpRequest.java
new file mode 100644
index 000000000..99d6eca8e
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpRequest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.v5;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.hc.client5.http.ClientProtocolException;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.routing.RoutingSupport;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.util.Timeout;
+
+public final class Apache5HttpRequest extends LowLevelHttpRequest {
+
+ private final HttpUriRequestBase request;
+
+ private final RequestConfig.Builder requestConfig;
+
+ private final HttpClient httpClient;
+
+ Apache5HttpRequest(HttpClient httpClient, HttpUriRequestBase request) {
+ this.httpClient = httpClient;
+ this.request = request;
+ // disable redirects as google-http-client handles redirects
+ this.requestConfig = RequestConfig.custom().setRedirectsEnabled(false);
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ request.addHeader(name, value);
+ }
+
+ @Override
+ public void setTimeout(int connectTimeout, int readTimeout) throws IOException {
+ requestConfig
+ .setConnectTimeout(Timeout.of(connectTimeout, TimeUnit.MILLISECONDS))
+ // ResponseTimeout behaves the same as 4.x's SocketTimeout
+ .setResponseTimeout(Timeout.of(readTimeout, TimeUnit.MILLISECONDS));
+ }
+
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ if (getStreamingContent() != null) {
+ Apache5ContentEntity entity =
+ new Apache5ContentEntity(
+ getContentLength(), getStreamingContent(), getContentType(), getContentEncoding());
+ request.setEntity(entity);
+ }
+ request.setConfig(requestConfig.build());
+ HttpHost target;
+ try {
+ target = RoutingSupport.determineHost(request);
+ } catch (HttpException e) {
+ throw new ClientProtocolException("The request's host is invalid.", e);
+ }
+ // we use a null context so the client creates the default one internally
+ ClassicHttpResponse httpResponse = httpClient.executeOpen(target, request, null);
+ return new Apache5HttpResponse(request, httpResponse);
+ }
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpResponse.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpResponse.java
new file mode 100644
index 000000000..1574c8c89
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpResponse.java
@@ -0,0 +1,102 @@
+/*
+ * 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.v5;
+
+import com.google.api.client.http.LowLevelHttpResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.message.StatusLine;
+
+final class Apache5HttpResponse extends LowLevelHttpResponse {
+
+ private static final Logger LOGGER = Logger.getLogger(Apache5HttpResponse.class.getName());
+ private final HttpUriRequestBase request;
+ private final ClassicHttpResponse response;
+ private final Header[] allHeaders;
+ private final HttpEntity entity;
+
+ Apache5HttpResponse(HttpUriRequestBase request, ClassicHttpResponse response) {
+ this.request = request;
+ this.response = response;
+ this.allHeaders = response.getHeaders();
+ this.entity = response.getEntity();
+ }
+
+ @Override
+ public int getStatusCode() {
+ return response.getCode();
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ return new Apache5ResponseContent(entity.getContent(), response);
+ }
+
+ @Override
+ public String getContentEncoding() {
+ return entity != null ? entity.getContentEncoding() : null;
+ }
+
+ @Override
+ public long getContentLength() {
+ return entity == null ? -1 : entity.getContentLength();
+ }
+
+ @Override
+ public String getContentType() {
+ return entity == null ? null : entity.getContentType();
+ }
+
+ @Override
+ public String getReasonPhrase() {
+ return response.getReasonPhrase();
+ }
+
+ @Override
+ public String getStatusLine() {
+ return new StatusLine(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();
+ }
+
+ /** Aborts execution of the request. */
+ @Override
+ public void disconnect() throws IOException {
+ request.abort();
+ response.close();
+ }
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java
new file mode 100644
index 000000000..868a2cf93
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java
@@ -0,0 +1,221 @@
+/*
+ * 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.v5;
+
+import com.google.api.client.http.HttpMethods;
+import com.google.api.client.http.HttpTransport;
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpDelete;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpHead;
+import org.apache.hc.client5.http.classic.methods.HttpOptions;
+import org.apache.hc.client5.http.classic.methods.HttpPatch;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.classic.methods.HttpTrace;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.io.ModalCloseable;
+
+/**
+ * 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
+ * #Apache5HttpTransport(HttpClient)} constructor to override the Apache HTTP Client used. Please
+ * read the
+ * Apache HTTP Client 5.x configuration example for more complex configuration options.
+ */
+public final class Apache5HttpTransport extends HttpTransport {
+
+ /** Apache HTTP client. */
+ private final HttpClient httpClient;
+
+ /** If the HTTP client uses mTLS channel. */
+ private final boolean isMtls;
+
+ /** Constructor that uses {@link #newDefaultHttpClient()} for the Apache HTTP client. */
+ public Apache5HttpTransport() {
+ this(newDefaultHttpClient(), false);
+ }
+
+ /**
+ * Constructor that allows an alternative Apache HTTP client to be used.
+ *
+ *
If you choose to provide your own Apache HttpClient implementation, be sure that
+ *
+ *
+ *
HTTP version is set to 1.1.
+ *
Retries are disabled (google-http-client handles retries).
+ *
+ *
+ * @param httpClient Apache HTTP client to use
+ */
+ public Apache5HttpTransport(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ this.isMtls = false;
+ }
+
+ /**
+ * {@link Beta}
+ * Constructor that allows an alternative CLoseable Apache HTTP client to be used.
+ *
+ *
If you choose to provide your own Apache HttpClient implementation, be sure that
+ *
+ *
+ *
HTTP version is set to 1.1.
+ *
Retries are disabled (google-http-client handles retries).
+ *
Redirects are disabled (google-http-client handles retries).
+ *
+ *
+ * @param httpClient Apache HTTP client to use
+ * @param isMtls If the HTTP client is mutual TLS
+ */
+ @Beta
+ public Apache5HttpTransport(HttpClient httpClient, boolean isMtls) {
+ this.httpClient = httpClient;
+ this.isMtls = isMtls;
+ }
+
+ /**
+ * Creates a new instance of the Apache HTTP client that is used by the {@link
+ * #Apache5HttpTransport()} constructor.
+ *
+ *
Settings:
+ *
+ *
+ *
The client connection manager is set to {@link PoolingHttpClientConnectionManager}.
+ *
The retry mechanism is turned off using {@link
+ * HttpClientBuilder#disableAutomaticRetries()}.
+ *
Redirects are turned off using {@link HttpClientBuilder#disableRedirectHandling}.
+ *
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
+ */
+ public static HttpClient newDefaultHttpClient() {
+ return newDefaultHttpClientBuilder().build();
+ }
+
+ /**
+ * Creates a new Apache HTTP client builder that is used by the {@link #Apache5HttpTransport()}
+ * constructor.
+ *
+ *
Settings:
+ *
+ *
+ *
The client connection manager is set to {@link PoolingHttpClientConnectionManager}.
+ *
The retry mechanism is turned off using {@link
+ * HttpClientBuilder#disableAutomaticRetries()}.
+ *
Redirects are turned off using {@link HttpClientBuilder#disableRedirectHandling}.
+ *
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 builder
+ */
+ public static HttpClientBuilder newDefaultHttpClientBuilder() {
+ PoolingHttpClientConnectionManager connectionManager =
+ PoolingHttpClientConnectionManagerBuilder.create()
+ .setSSLSocketFactory(SSLConnectionSocketFactory.getSocketFactory())
+ .setMaxConnTotal(200)
+ .setMaxConnPerRoute(20)
+ .setDefaultConnectionConfig(
+ ConnectionConfig.custom().setTimeToLive(-1, TimeUnit.MILLISECONDS).build())
+ .build();
+
+ return HttpClients.custom()
+ .useSystemProperties()
+ .setConnectionManager(connectionManager)
+ .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
+ .disableRedirectHandling()
+ .disableAutomaticRetries();
+ }
+
+ @Override
+ public boolean supportsMethod(String method) {
+ return true;
+ }
+
+ @Override
+ protected Apache5HttpRequest buildRequest(String method, String url) {
+ HttpUriRequestBase requestBase;
+ if (method.equals(HttpMethods.DELETE)) {
+ requestBase = new HttpDelete(url);
+ } else if (method.equals(HttpMethods.GET)) {
+ requestBase = new HttpGet(url);
+ } else if (method.equals(HttpMethods.HEAD)) {
+ requestBase = new HttpHead(url);
+ } else if (method.equals(HttpMethods.PATCH)) {
+ requestBase = new HttpPatch(url);
+ } else if (method.equals(HttpMethods.POST)) {
+ requestBase = new HttpPost(url);
+ } else if (method.equals(HttpMethods.PUT)) {
+ requestBase = new HttpPut(url);
+ } else if (method.equals(HttpMethods.TRACE)) {
+ requestBase = new HttpTrace(url);
+ } else if (method.equals(HttpMethods.OPTIONS)) {
+ requestBase = new HttpOptions(url);
+ } else {
+ requestBase = new HttpUriRequestBase(Preconditions.checkNotNull(method), URI.create(url));
+ }
+ return new Apache5HttpRequest(httpClient, requestBase);
+ }
+
+ /**
+ * Gracefully shuts down the connection manager and releases allocated resources. This closes all
+ * connections, whether they are currently used or not.
+ */
+ @Override
+ public void shutdown() throws IOException {
+ if (httpClient instanceof ModalCloseable) {
+ ((ModalCloseable) httpClient).close(CloseMode.GRACEFUL);
+ }
+ // otherwise no-op
+ }
+
+ /** Returns the Apache HTTP client. */
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+
+ /** Returns if the underlying HTTP client is mTLS. */
+ @Override
+ public boolean isMtls() {
+ return isMtls;
+ }
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ResponseContent.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ResponseContent.java
new file mode 100644
index 000000000..c2d3091df
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ResponseContent.java
@@ -0,0 +1,75 @@
+package com.google.api.client.http.apache.v5;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpResponse;
+
+/**
+ * Class that wraps an {@link org.apache.hc.core5.http.HttpEntity}'s content {@link InputStream}
+ * along with the {@link ClassicHttpResponse} that contains this entity. The main purpose is to be
+ * able to close the response as well as the content input stream when {@link #close()} is called,
+ * in order to not break the existing contract with clients using apache v4 that only required them
+ * to close the input stream to clean up all resources.
+ */
+public class Apache5ResponseContent extends InputStream {
+ private final ClassicHttpResponse response;
+ private final InputStream wrappedStream;
+
+ public Apache5ResponseContent(InputStream wrappedStream, ClassicHttpResponse response) {
+ this.response = response;
+ this.wrappedStream = wrappedStream;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return wrappedStream.read();
+ }
+
+ @Override
+ public int read(byte b[]) throws IOException {
+ return wrappedStream.read(b);
+ }
+
+ @Override
+ public int read(byte b[], int off, int len) throws IOException {
+ return wrappedStream.read(b, off, len);
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ return wrappedStream.skip(n);
+ }
+
+ @Override
+ public int available() throws IOException {
+ return wrappedStream.available();
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ wrappedStream.mark(readlimit);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ wrappedStream.reset();
+ }
+
+ @Override
+ public void close() throws IOException {
+ wrappedStream.close();
+ response.close();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return wrappedStream.markSupported();
+ }
+
+ @VisibleForTesting
+ HttpResponse getResponse() {
+ return response;
+ }
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/package-info.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/package-info.java
new file mode 100644
index 000000000..223edc82d
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/package-info.java
@@ -0,0 +1,16 @@
+/*
+ * 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/Core version 5.x */
+package com.google.api.client.http.apache.v5;
diff --git a/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpRequestTest.java b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpRequestTest.java
new file mode 100644
index 000000000..3b7ca4a21
--- /dev/null
+++ b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpRequestTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.v5;
+
+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.http.LowLevelHttpResponse;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.io.entity.BasicHttpEntity;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.junit.Test;
+
+public class Apache5HttpRequestTest {
+ @Test
+ public void testContentLengthSet() throws Exception {
+ HttpUriRequestBase base = new HttpPost("http://www.google.com");
+ Apache5HttpRequest request =
+ new Apache5HttpRequest(
+ new MockHttpClient() {
+ @Override
+ public ClassicHttpResponse executeOpen(
+ HttpHost target, ClassicHttpRequest request, HttpContext context) {
+ return new MockClassicHttpResponse();
+ }
+ },
+ 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) ' ');
+ HttpUriRequestBase base = new HttpPost("http://www.google.com");
+ Apache5HttpRequest request =
+ new Apache5HttpRequest(
+ new MockHttpClient() {
+ @Override
+ public ClassicHttpResponse executeOpen(
+ HttpHost target, ClassicHttpRequest request, HttpContext context) {
+ return new MockClassicHttpResponse();
+ }
+ },
+ base);
+ HttpContent content = new InputStreamContent("text/plain", new ByteArrayInputStream(buf));
+ request.setStreamingContent(content);
+ request.execute();
+
+ assertTrue(base.getEntity().isChunked());
+ assertEquals(-1, base.getEntity().getContentLength());
+ }
+
+ @Test
+ public void testExecute_closeContent_closesResponse() throws Exception {
+ HttpUriRequestBase base = new HttpPost("http://www.google.com");
+ final InputStream responseContentStream = new ByteArrayInputStream(new byte[] {1, 2, 3});
+ BasicHttpEntity testEntity =
+ new BasicHttpEntity(responseContentStream, ContentType.DEFAULT_BINARY);
+ AtomicInteger closedResponseCounter = new AtomicInteger(0);
+ ClassicHttpResponse classicResponse =
+ new MockClassicHttpResponse() {
+ @Override
+ public HttpEntity getEntity() {
+ return testEntity;
+ }
+
+ @Override
+ public void close() {
+ closedResponseCounter.incrementAndGet();
+ }
+ };
+
+ Apache5HttpRequest request =
+ new Apache5HttpRequest(
+ new MockHttpClient() {
+ @Override
+ public ClassicHttpResponse executeOpen(
+ HttpHost target, ClassicHttpRequest request, HttpContext context) {
+ return classicResponse;
+ }
+ },
+ base);
+ LowLevelHttpResponse response = request.execute();
+ assertTrue(response instanceof Apache5HttpResponse);
+
+ // we confirm that the classic response we prepared in this test is the same as the content's
+ // response
+ assertTrue(response.getContent() instanceof Apache5ResponseContent);
+ assertEquals(classicResponse, ((Apache5ResponseContent) response.getContent()).getResponse());
+
+ // we close the response's content stream and confirm the response is also closed
+ assertEquals(0, closedResponseCounter.get());
+ response.getContent().close();
+ assertEquals(1, closedResponseCounter.get());
+ }
+}
diff --git a/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpTransportTest.java b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpTransportTest.java
new file mode 100644
index 000000000..99045d99d
--- /dev/null
+++ b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpTransportTest.java
@@ -0,0 +1,353 @@
+/*
+ * 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.v5;
+
+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.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.util.ByteArrayStreamingContent;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hc.client5.http.ConnectTimeoutException;
+import org.apache.hc.client5.http.HttpHostConnectException;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpRequestMapper;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
+import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
+import org.apache.hc.core5.http.impl.io.HttpService;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
+import org.apache.hc.core5.http.io.support.BasicHttpServerRequestHandler;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.junit.Assert;
+import org.junit.Test;
+
+/** Tests {@link Apache5HttpTransport}. */
+public class Apache5HttpTransportTest {
+
+ @Test
+ public void testApacheHttpTransport() {
+ Apache5HttpTransport transport = new Apache5HttpTransport();
+ checkHttpTransport(transport);
+ assertFalse(transport.isMtls());
+ }
+
+ @Test
+ public void testApacheHttpTransportWithParam() {
+ Apache5HttpTransport transport = new Apache5HttpTransport(HttpClients.custom().build(), true);
+ checkHttpTransport(transport);
+ assertTrue(transport.isMtls());
+ }
+
+ @Test
+ public void testNewDefaultHttpClient() {
+ HttpClient client = Apache5HttpTransport.newDefaultHttpClient();
+ checkHttpClient(client);
+ }
+
+ private void checkHttpTransport(Apache5HttpTransport transport) {
+ assertNotNull(transport);
+ HttpClient client = transport.getHttpClient();
+ checkHttpClient(client);
+ }
+
+ private void checkHttpClient(HttpClient 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 {
+ // This test confirms that we can set the content on any type of request
+ HttpClient mockClient =
+ new MockHttpClient() {
+ @Override
+ public ClassicHttpResponse executeOpen(
+ HttpHost target, ClassicHttpRequest request, HttpContext context) {
+ return new MockClassicHttpResponse();
+ }
+ };
+ Apache5HttpTransport transport = new Apache5HttpTransport(mockClient);
+
+ // Test GET.
+ execute(transport.buildRequest("GET", "http://www.test.url"));
+ // Test DELETE.
+ execute(transport.buildRequest("DELETE", "http://www.test.url"));
+ // Test HEAD.
+ execute(transport.buildRequest("HEAD", "http://www.test.url"));
+
+ // 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 execute(Apache5HttpRequest 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 MockClassicHttpResponse();
+ response.setCode(302);
+ response.setReasonPhrase(null);
+ response.addHeader("location", "https://google.com/path");
+ response.addHeader(HttpHeaders.SET_COOKIE, "");
+ requestsAttempted.incrementAndGet();
+ return response;
+ }
+ };
+ HttpClient client = HttpClients.custom().setRequestExecutor(requestExecutor).build();
+ Apache5HttpTransport transport = new Apache5HttpTransport(client);
+ Apache5HttpRequest 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()
+ .addRequestInterceptorFirst(
+ new HttpRequestInterceptor() {
+ @Override
+ public void process(
+ HttpRequest request, EntityDetails details, 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();
+
+ Apache5HttpTransport transport = new Apache5HttpTransport(client);
+ Apache5HttpRequest 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() {
+ // 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 Apache5HttpTransport();
+ 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;
+
+ FakeServer(final HttpRequestHandler httpHandler) throws IOException {
+ HttpRequestMapper mapper =
+ new HttpRequestMapper() {
+ @Override
+ public HttpRequestHandler resolve(HttpRequest request, HttpContext context)
+ throws HttpException {
+ return httpHandler;
+ };
+ };
+ server =
+ new HttpServer(
+ 0,
+ HttpService.builder()
+ .withHttpProcessor(
+ new HttpProcessor() {
+ @Override
+ public void process(
+ HttpRequest request, EntityDetails entity, HttpContext context)
+ throws HttpException, IOException {}
+
+ @Override
+ public void process(
+ HttpResponse response, EntityDetails entity, HttpContext context)
+ throws HttpException, IOException {}
+ })
+ .withHttpServerRequestHandler(new BasicHttpServerRequestHandler(mapper))
+ .build(),
+ null,
+ null,
+ null,
+ null,
+ null,
+ null);
+ // server.createContext("/", httpHandler);
+ server.start();
+ }
+
+ public int getPort() {
+ return server.getLocalPort();
+ }
+
+ @Override
+ public void close() {
+ server.initiateShutdown();
+ }
+ }
+
+ @Test
+ public void testNormalizedUrl() throws IOException {
+ final HttpRequestHandler handler =
+ new HttpRequestHandler() {
+ @Override
+ public void handle(
+ ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ // Extract the request URI and convert to bytes
+ byte[] responseData = request.getRequestUri().getBytes(StandardCharsets.UTF_8);
+
+ // Set the response headers (status code and content length)
+ response.setCode(HttpStatus.SC_OK);
+ response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length));
+
+ // Set the response entity (body)
+ ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN);
+ response.setEntity(entity);
+ }
+ };
+ try (FakeServer server = new FakeServer(handler)) {
+ HttpTransport transport = new Apache5HttpTransport();
+ 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 HttpRequestHandler handler =
+ new HttpRequestHandler() {
+ @Override
+ public void handle(
+ ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ byte[] responseData = "Forbidden".getBytes(StandardCharsets.UTF_8);
+ response.setCode(HttpStatus.SC_FORBIDDEN); // 403 Forbidden
+ response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length));
+ ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN);
+ response.setEntity(entity);
+ }
+ };
+ try (FakeServer server = new FakeServer(handler)) {
+ HttpTransport transport = new Apache5HttpTransport();
+ 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 HttpRequestHandler handler =
+ new HttpRequestHandler() {
+ @Override
+ public void handle(
+ ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ byte[] responseData = "Forbidden".getBytes(StandardCharsets.UTF_8);
+ response.setCode(HttpStatus.SC_FORBIDDEN); // 403 Forbidden
+ response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length));
+ ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN);
+ response.setEntity(entity);
+ }
+ };
+ try (FakeServer server = new FakeServer(handler)) {
+ HttpTransport transport = new Apache5HttpTransport();
+ 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-v5/src/test/java/com/google/api/client/http/apache/v5/MockClassicHttpResponse.java b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockClassicHttpResponse.java
new file mode 100644
index 000000000..091721745
--- /dev/null
+++ b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockClassicHttpResponse.java
@@ -0,0 +1,182 @@
+package com.google.api.client.http.apache.v5;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.ProtocolVersion;
+
+public class MockClassicHttpResponse implements ClassicHttpResponse {
+ List headers = new ArrayList<>();
+ int code = 200;
+
+ @Override
+ public int getCode() {
+ return code;
+ }
+
+ @Override
+ public void setCode(int code) {
+ this.code = code;
+ }
+
+ @Override
+ public String getReasonPhrase() {
+ return null;
+ }
+
+ @Override
+ public void setReasonPhrase(String reason) {}
+
+ @Override
+ public Locale getLocale() {
+ return null;
+ }
+
+ @Override
+ public void setLocale(Locale loc) {}
+
+ @Override
+ public void setVersion(ProtocolVersion version) {}
+
+ @Override
+ public ProtocolVersion getVersion() {
+ return HttpVersion.HTTP_1_1;
+ }
+
+ @Override
+ public void addHeader(Header header) {
+ headers.add(header);
+ }
+
+ @Override
+ public void addHeader(String name, Object value) {
+ addHeader(newHeader(name, value));
+ }
+
+ private Header newHeader(String key, Object value) {
+ return new Header() {
+ @Override
+ public boolean isSensitive() {
+ return false;
+ }
+
+ @Override
+ public String getName() {
+ return key;
+ }
+
+ @Override
+ public String getValue() {
+ return value.toString();
+ }
+ };
+ }
+
+ @Override
+ public void setHeader(Header header) {
+ if (headers.contains(header)) {
+ int index = headers.indexOf(header);
+ headers.set(index, header);
+ } else {
+ addHeader(header);
+ }
+ }
+
+ @Override
+ public void setHeader(String name, Object value) {
+ setHeader(newHeader(name, value));
+ }
+
+ @Override
+ public void setHeaders(Header... headers) {
+ for (Header header : headers) {
+ setHeader(header);
+ }
+ }
+
+ @Override
+ public boolean removeHeader(Header header) {
+ if (headers.contains(header)) {
+ headers.remove(headers.indexOf(header));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeHeaders(String name) {
+ int initialSize = headers.size();
+ for (Header header :
+ headers.stream().filter(h -> h.getName() == name).collect(Collectors.toList())) {
+ removeHeader(header);
+ }
+ return headers.size() < initialSize;
+ }
+
+ @Override
+ public boolean containsHeader(String name) {
+ return headers.stream().anyMatch(h -> h.getName() == name);
+ }
+
+ @Override
+ public int countHeaders(String name) {
+ return headers.size();
+ }
+
+ @Override
+ public Header getFirstHeader(String name) {
+ return headers.stream().findFirst().orElse(null);
+ }
+
+ @Override
+ public Header getHeader(String name) throws ProtocolException {
+ return headers.stream().filter(h -> h.getName() == name).findFirst().orElse(null);
+ }
+
+ @Override
+ public Header[] getHeaders() {
+ return headers.toArray(new Header[0]);
+ }
+
+ @Override
+ public Header[] getHeaders(String name) {
+ return headers.stream()
+ .filter(h -> h.getName() == name)
+ .collect(Collectors.toList())
+ .toArray(new Header[0]);
+ }
+
+ @Override
+ public Header getLastHeader(String name) {
+ return headers.isEmpty() ? null : headers.get(headers.size() - 1);
+ }
+
+ @Override
+ public Iterator headerIterator() {
+ return headers.iterator();
+ }
+
+ @Override
+ public Iterator headerIterator(String name) {
+ return headers.stream().filter(h -> h.getName() == name).iterator();
+ }
+
+ @Override
+ public void close() throws IOException {}
+
+ @Override
+ public HttpEntity getEntity() {
+ return null;
+ }
+
+ @Override
+ public void setEntity(HttpEntity entity) {}
+}
diff --git a/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockHttpClient.java b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockHttpClient.java
new file mode 100644
index 000000000..8d26096cf
--- /dev/null
+++ b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockHttpClient.java
@@ -0,0 +1,86 @@
+package com.google.api.client.http.apache.v5;
+
+import com.google.api.client.util.Preconditions;
+import java.io.IOException;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+public class MockHttpClient implements HttpClient {
+
+ /** HTTP response code to use. */
+ int responseCode;
+
+ /** Returns the HTTP response code to use. */
+ public final int getResponseCode() {
+ return responseCode;
+ }
+
+ /** Sets the HTTP response code to use. */
+ public MockHttpClient setResponseCode(int responseCode) {
+ Preconditions.checkArgument(responseCode >= 0);
+ this.responseCode = responseCode;
+ return this;
+ }
+
+ @Override
+ public HttpResponse execute(ClassicHttpRequest request) throws IOException {
+ return null;
+ }
+
+ @Override
+ public HttpResponse execute(ClassicHttpRequest request, HttpContext context) throws IOException {
+ return null;
+ }
+
+ @Override
+ public ClassicHttpResponse execute(HttpHost target, ClassicHttpRequest request)
+ throws IOException {
+ return null;
+ }
+
+ @Override
+ public HttpResponse execute(HttpHost target, ClassicHttpRequest request, HttpContext context)
+ throws IOException {
+ return null;
+ }
+
+ @Override
+ public T execute(
+ ClassicHttpRequest request, HttpClientResponseHandler extends T> responseHandler)
+ throws IOException {
+ return null;
+ }
+
+ @Override
+ public T execute(
+ ClassicHttpRequest request,
+ HttpContext context,
+ HttpClientResponseHandler extends T> responseHandler)
+ throws IOException {
+ return null;
+ }
+
+ @Override
+ public T execute(
+ HttpHost target,
+ ClassicHttpRequest request,
+ HttpClientResponseHandler extends T> responseHandler)
+ throws IOException {
+ return null;
+ }
+
+ @Override
+ public T execute(
+ HttpHost target,
+ ClassicHttpRequest request,
+ HttpContext context,
+ HttpClientResponseHandler extends T> responseHandler)
+ throws IOException {
+ return null;
+ }
+}
diff --git a/google-http-client-assembly/classpath-include b/google-http-client-assembly/classpath-include
index c1bd80328..c7bbd573f 100644
--- a/google-http-client-assembly/classpath-include
+++ b/google-http-client-assembly/classpath-include
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/google-http-client-assembly/readme.html b/google-http-client-assembly/readme.html
index 5e7af564d..8a146df1e 100644
--- a/google-http-client-assembly/readme.html
+++ b/google-http-client-assembly/readme.html
@@ -135,8 +135,8 @@