diff --git a/google-http-client-apache-v3/pom.xml b/google-http-client-apache-v3/pom.xml
new file mode 100644
index 000000000..4b6f47e44
--- /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
+ httpcore
+
+
+
+
+ junit
+ junit
+ test
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+
+
+ 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/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..7197f7715
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequest.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.google.api.client.util.Preconditions;
+import java.io.IOException;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpRequestBase;
+
+/** @author Yaniv Inbar */
+final class ApacheHttpRequest extends LowLevelHttpRequest {
+ private final HttpClient httpClient;
+
+ private final HttpRequestBase request;
+
+ private RequestConfig.Builder requestConfig;
+
+ @SuppressWarnings("deprecation")
+ ApacheHttpRequest(HttpClient httpClient, HttpRequestBase request) {
+ this.httpClient = httpClient;
+ this.request = request;
+ // disable redirects as google-http-client handles redirects
+ this.requestConfig =
+ RequestConfig.custom()
+ .setRedirectsEnabled(false)
+ .setNormalizeUri(false)
+ // TODO(chingor): configure in HttpClientBuilder when available
+ .setStaleConnectionCheckEnabled(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(connectTimeout).setSocketTimeout(readTimeout);
+ }
+
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ if (getStreamingContent() != null) {
+ Preconditions.checkState(
+ request instanceof HttpEntityEnclosingRequest,
+ "Apache HTTP client does not support %s requests with content.",
+ request.getRequestLine().getMethod());
+ ContentEntity entity = new ContentEntity(getContentLength(), getStreamingContent());
+ entity.setContentEncoding(getContentEncoding());
+ entity.setContentType(getContentType());
+ if (getContentLength() == -1) {
+ entity.setChunked(true);
+ }
+ ((HttpEntityEnclosingRequest) request).setEntity(entity);
+ }
+ request.setConfig(requestConfig.build());
+ return new ApacheHttpResponse(request, httpClient.execute(request));
+ }
+}
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..4983013ac
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpResponse.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.LowLevelHttpResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.HttpRequestBase;
+
+final class ApacheHttpResponse extends LowLevelHttpResponse {
+
+ private final HttpRequestBase request;
+ private final HttpResponse response;
+ private final Header[] allHeaders;
+
+ ApacheHttpResponse(HttpRequestBase request, HttpResponse response) {
+ this.request = request;
+ this.response = response;
+ allHeaders = response.getAllHeaders();
+ }
+
+ @Override
+ public int getStatusCode() {
+ StatusLine statusLine = response.getStatusLine();
+ return statusLine == null ? 0 : statusLine.getStatusCode();
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ HttpEntity entity = response.getEntity();
+ return entity == null ? null : entity.getContent();
+ }
+
+ @Override
+ public String getContentEncoding() {
+ HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ Header contentEncodingHeader = entity.getContentEncoding();
+ if (contentEncodingHeader != null) {
+ return contentEncodingHeader.getValue();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public long getContentLength() {
+ HttpEntity entity = response.getEntity();
+ return entity == null ? -1 : entity.getContentLength();
+ }
+
+ @Override
+ public String getContentType() {
+ HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ Header contentTypeHeader = entity.getContentType();
+ if (contentTypeHeader != null) {
+ return contentTypeHeader.getValue();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getReasonPhrase() {
+ StatusLine statusLine = response.getStatusLine();
+ return statusLine == null ? null : statusLine.getReasonPhrase();
+ }
+
+ @Override
+ public String getStatusLine() {
+ StatusLine statusLine = response.getStatusLine();
+ return statusLine == null ? null : statusLine.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.
+ *
+ * @since 1.30
+ */
+ @Override
+ public void disconnect() {
+ request.abort();
+ }
+}
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..aedb66250
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpTransport.java
@@ -0,0 +1,231 @@
+/*
+ * 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.HttpMethods;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.util.Beta;
+import java.io.IOException;
+import java.net.ProxySelector;
+import java.util.concurrent.TimeUnit;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
+
+/**
+ * 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(HttpClient)} constructor to override the Apache HTTP Client used. Please
+ * read the
+ * Apache HTTP Client connection management tutorial for more complex configuration options.
+ *
+ * @since 1.30
+ * @author Yaniv Inbar
+ */
+public final class ApacheHttpTransport 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.
+ *
+ * @since 1.30
+ */
+ public ApacheHttpTransport() {
+ this(newDefaultHttpClient(), 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 httpClient Apache HTTP client to use
+ * @since 1.30
+ */
+ public ApacheHttpTransport(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ 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 httpClient Apache HTTP client to use
+ * @param isMtls If the HTTP client is mutual TLS
+ * @since 1.38
+ */
+ @Beta
+ public ApacheHttpTransport(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
+ * #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 HttpClient 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 HttpClientBuilder newDefaultHttpClientBuilder() {
+
+ return HttpClientBuilder.create()
+ .useSystemProperties()
+ .setSSLSocketFactory(SSLConnectionSocketFactory.getSocketFactory())
+ .setMaxConnTotal(200)
+ .setMaxConnPerRoute(20)
+ .setConnectionTimeToLive(-1, TimeUnit.MILLISECONDS)
+ .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
+ .disableRedirectHandling()
+ .disableAutomaticRetries();
+ }
+
+ @Override
+ public boolean supportsMethod(String method) {
+ return true;
+ }
+
+ @Override
+ protected ApacheHttpRequest buildRequest(String method, String url) {
+ HttpRequestBase 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 HttpExtensionMethod(method, url);
+ }
+ return new ApacheHttpRequest(httpClient, requestBase);
+ }
+
+ /**
+ * 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 {
+ if (httpClient instanceof CloseableHttpClient) {
+ ((CloseableHttpClient) httpClient).close();
+ }
+ }
+
+ /**
+ * Returns the Apache HTTP client.
+ *
+ * @since 1.30
+ */
+ 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-v3/src/main/java/com/google/api/client/http/apache/v3/ContentEntity.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ContentEntity.java
new file mode 100644
index 000000000..e7f26f017
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ContentEntity.java
@@ -0,0 +1,68 @@
+/*
+ * 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.util.Preconditions;
+import com.google.api.client.util.StreamingContent;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.http.entity.AbstractHttpEntity;
+
+/** @author Yaniv Inbar */
+final class ContentEntity 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
+ */
+ ContentEntity(long contentLength, StreamingContent streamingContent) {
+ 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);
+ }
+ }
+}
diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/HttpExtensionMethod.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/HttpExtensionMethod.java
new file mode 100644
index 000000000..ec139522f
--- /dev/null
+++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/HttpExtensionMethod.java
@@ -0,0 +1,44 @@
+/*
+ * 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.util.Preconditions;
+import java.net.URI;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+
+/**
+ * HTTP extension method.
+ *
+ * @author Yaniv Inbar
+ */
+final class HttpExtensionMethod extends HttpEntityEnclosingRequestBase {
+
+ /** Request method name. */
+ private final String methodName;
+
+ /**
+ * @param methodName request method name
+ * @param uri URI
+ */
+ public HttpExtensionMethod(String methodName, String uri) {
+ this.methodName = Preconditions.checkNotNull(methodName);
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return methodName;
+ }
+}
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..c47f7d6cd
--- /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..c6e8a1ad1
--- /dev/null
+++ b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpTransportTest.java
@@ -0,0 +1,341 @@
+/*
+ * 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.http.Header;
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.conn.HttpHostConnectException;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestExecutor;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests {@link ApacheHttpTransport}.
+ *
+ * @author Yaniv Inbar
+ */
+public class ApacheHttpTransportTest {
+
+ private static class MockHttpResponse extends BasicHttpResponse implements CloseableHttpResponse {
+ public MockHttpResponse() {
+ super(HttpVersion.HTTP_1_1, 200, "OK");
+ }
+
+ @Override
+ public void close() throws IOException {}
+ }
+
+ @Test
+ public void testApacheHttpTransport() {
+ ApacheHttpTransport transport = new ApacheHttpTransport();
+ checkHttpTransport(transport);
+ assertFalse(transport.isMtls());
+ }
+
+ @Test
+ public void testApacheHttpTransportWithParam() {
+ ApacheHttpTransport transport = new ApacheHttpTransport(HttpClients.custom().build(), true);
+ checkHttpTransport(transport);
+ assertTrue(transport.isMtls());
+ }
+
+ @Test
+ public void testNewDefaultHttpClient() {
+ HttpClient client = ApacheHttpTransport.newDefaultHttpClient();
+ checkHttpClient(client);
+ }
+
+ private void checkHttpTransport(ApacheHttpTransport 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 {
+ 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 HttpResponse execute(
+ HttpRequest request, HttpClientConnection connection, HttpContext context)
+ throws IOException, HttpException {
+ HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, 302, null);
+ response.addHeader("location", "https://google.com/path");
+ requestsAttempted.incrementAndGet();
+ return response;
+ }
+ };
+ HttpClient client = HttpClients.custom().setRequestExecutor(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/pom.xml b/pom.xml
index cdc778f9f..19b841233 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
google-http-client-appenginegoogle-http-client-androidgoogle-http-client-apache-v2
+ google-http-client-apache-v3google-http-client-protobufgoogle-http-client-gsongoogle-http-client-jackson2
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