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 + * + *

+ * + * @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 + * + *

+ * + * @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: + * + *

+ * + * @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: + * + *

+ * + * @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-appengine google-http-client-android google-http-client-apache-v2 + google-http-client-apache-v3 google-http-client-protobuf google-http-client-gson google-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