diff --git a/google-http-client-apache-v3/pom.xml b/google-http-client-apache-v3/pom.xml new file mode 100644 index 000000000..4c0dbae9e --- /dev/null +++ b/google-http-client-apache-v3/pom.xml @@ -0,0 +1,115 @@ + + 4.0.0 + + com.google.http-client + google-http-client-parent + 1.44.3-SNAPSHOT + ../pom.xml + + google-http-client-apache-v3 + 1.44.3-SNAPSHOT + Apache HTTP transport v3 for the Google HTTP Client Library for Java. + + + + + maven-javadoc-plugin + + + https://download.oracle.com/javase/7/docs/api/ + + ${project.name} ${project.version} + ${project.artifactId} ${project.version} + + + + maven-source-plugin + + + org.codehaus.mojo + build-helper-maven-plugin + 3.3.0 + + + add-test-source + generate-test-sources + + add-test-source + + + + target/generated-test-sources + + + + + + + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + com.google.api.client.http.apache.v3 + + + + + + org.apache.felix + maven-bundle-plugin + 5.1.9 + + + bundle-manifest + process-classes + + manifest + + + + + + maven-compiler-plugin + 3.13.0 + + 1.8 + 1.8 + + + + + + + com.google.http-client + google-http-client + + + org.apache.httpcomponents + httpclient + + + + + com.google.firebase + firebase-admin + 9.3.0 + + + junit + junit + test + + + org.apache.httpcomponents.client5 + httpclient5 + 5.3.1 + + + org.apache.httpcomponents.core5 + httpcore5-h2 + 5.2.4 + + + diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Request.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Request.java new file mode 100644 index 000000000..c2b6016b8 --- /dev/null +++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Request.java @@ -0,0 +1,92 @@ +package com.google.api.client.http.apache.v3; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; +import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.util.Timeout; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; + +@SuppressWarnings("deprecation") +final class ApacheHttp2Request extends LowLevelHttpRequest { + private final CloseableHttpAsyncClient httpAsyncClient; + private final SimpleRequestBuilder requestBuilder; + private SimpleHttpRequest request; + private final RequestConfig.Builder requestConfig; + + ApacheHttp2Request(CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) { + this.httpAsyncClient = httpAsyncClient; + this.requestBuilder = requestBuilder; + + this.requestConfig = RequestConfig.custom() + .setRedirectsEnabled(false); + } + + @Override + public void addHeader(String name, String value) { + requestBuilder.addHeader(name, value); + } + + @Override + public void setTimeout(int connectionTimeout, int readTimeout) throws IOException { + requestConfig + .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)) + .setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); + // .setConnectionRequestTimeout(connectionTimeout) + // .setResponseTimeout(); + } + + @Override + public LowLevelHttpResponse execute() throws IOException { + // Convert StreamingContent to bytes to set request body + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + getStreamingContent().writeTo(baos); + byte[] bytes = baos.toByteArray(); + requestBuilder.setBody(bytes, ContentType.parse(getContentType())); + + // Set request configs + requestBuilder.setRequestConfig(requestConfig.build()); + + // Build and execute request + request = requestBuilder.build(); + final CompletableFuture responseFuture = new CompletableFuture<>(); + try { + httpAsyncClient.execute( + SimpleRequestProducer.create(request), + SimpleResponseConsumer.create(), + new FutureCallback() { + @Override + public void completed(final SimpleHttpResponse response) { + responseFuture.complete(response); + } + + @Override + public void failed(final Exception exception) { + responseFuture.completeExceptionally(exception); + } + + @Override + public void cancelled() { + responseFuture.cancel(false); + } + }); + final SimpleHttpResponse response = responseFuture.get(); + return new ApacheHttp2Response(request, response); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + throw new IOException("Error making request", e); + } + } +} diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Response.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Response.java new file mode 100644 index 000000000..1937b69fc --- /dev/null +++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Response.java @@ -0,0 +1,76 @@ +package com.google.api.client.http.apache.v3; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.http.Header; + +import com.google.api.client.http.LowLevelHttpResponse; + +public class ApacheHttp2Response extends LowLevelHttpResponse { + + private final SimpleHttpResponse response; + private final Header[] allHeaders; + + ApacheHttp2Response(SimpleHttpRequest request, SimpleHttpResponse response) { + this.response = response; + allHeaders = response.getHeaders(); + } + + @Override + public int getStatusCode() { + return response.getCode(); + } + + @Override + public InputStream getContent() throws IOException { + return new ByteArrayInputStream(response.getBodyBytes()); + } + + @Override + public String getContentEncoding() { + return response.getFirstHeader("Content-Encoding").getValue(); + } + + @Override + public long getContentLength() { + return response.getBodyText().length(); + } + + @Override + public String getContentType() { + return response.getContentType().toString(); + } + + @Override + public String getReasonPhrase() { + return response.getReasonPhrase(); + } + + @Override + public String getStatusLine() { + return response.toString(); + } + + public String getHeaderValue(String name) { + return response.getLastHeader(name).getValue(); + } + + @Override + public int getHeaderCount() { + return allHeaders.length; + } + + @Override + public String getHeaderName(int index) { + return allHeaders[index].getName(); + } + + @Override + public String getHeaderValue(int index) { + return allHeaders[index].getValue(); + } +} diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Transport.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Transport.java new file mode 100644 index 000000000..245953ead --- /dev/null +++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttp2Transport.java @@ -0,0 +1,110 @@ +package com.google.api.client.http.apache.v3; + +import java.io.IOException; +import java.net.ProxySelector; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.async.HttpAsyncClient; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.TlsConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.core5.http.config.Http1Config; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.reactor.IOReactorConfig; + +import com.google.api.client.http.HttpTransport; + +public final class ApacheHttp2Transport extends HttpTransport{ + + public final CloseableHttpAsyncClient httpAsyncClient; + + public ApacheHttp2Transport() { + this(newDefaultHttpAsyncClient(false)); + } + + public ApacheHttp2Transport(Boolean useCustom) { + this(newDefaultHttpAsyncClient(useCustom)); + } + + public ApacheHttp2Transport(CloseableHttpAsyncClient httpAsyncClient) { + this.httpAsyncClient = httpAsyncClient; + httpAsyncClient.start(); + } + + public static CloseableHttpAsyncClient newDefaultHttpAsyncClient(Boolean useCustom) { + if (useCustom) { + return defaultHttpAsyncClientBuilder().build(); + } + return HttpAsyncClients.createHttp2System(); + } + + public static HttpAsyncClientBuilder defaultHttpAsyncClientBuilder() { + return HttpAsyncClientBuilder.create() + .setH2Config(H2Config.DEFAULT) + .setHttp1Config(Http1Config.DEFAULT) + .setIOReactorConfig(IOReactorConfig.DEFAULT) + .setConnectionManager(defaultAsyncClientConnectionManager()) + .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault())) + .disableRedirectHandling() + .disableAutomaticRetries(); + } + + public static AsyncClientConnectionManager defaultAsyncClientConnectionManager() { + return defaultPoolingAsyncClientConnectionManagerBuilder() + .build(); + } + + public static PoolingAsyncClientConnectionManagerBuilder defaultPoolingAsyncClientConnectionManagerBuilder() { + return PoolingAsyncClientConnectionManagerBuilder + .create() + .useSystemProperties() + // .setConnectionConfigResolver(null) + .setDefaultConnectionConfig(defaultConnectionConfig()) + // .setTlsConfigResolver(null) + .setDefaultTlsConfig(defaultTlsConfig()) + .setTlsStrategy(null) + .setMaxConnTotal(200) + .setMaxConnPerRoute(20); + } + + public static TlsConfig defaultTlsConfig() { + return TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2).build(); + } + + public static ConnectionConfig defaultConnectionConfig() { + return ConnectionConfig.custom() + // .setConnectTimeout(null) + // .setSocketTimeout(null) + .setTimeToLive(-1, TimeUnit.MILLISECONDS) + .build(); + } + + @Override + public boolean supportsMethod(String method) { + return true; + } + + @Override + protected ApacheHttp2Request buildRequest(String method, String url) { + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(method).setUri(url); + return new ApacheHttp2Request(httpAsyncClient, requestBuilder); + } + + @Override + public void shutdown() throws IOException { + if (httpAsyncClient instanceof CloseableHttpAsyncClient) { + ((CloseableHttpAsyncClient) httpAsyncClient).close(); + } + } + + public HttpAsyncClient getHttpClient() { + return httpAsyncClient; + } +} diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequest.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequest.java new file mode 100644 index 000000000..76f0ad644 --- /dev/null +++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.api.client.http.apache.v3; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; +import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer; +import org.apache.hc.core5.http.nio.support.AbstractAsyncResponseConsumer; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.util.Timeout; +import org.apache.http.HttpEntity; + +/** + * @author Yaniv Inbar + */ +final class ApacheHttpRequest extends LowLevelHttpRequest { + private final HttpAsyncClientBuilder httpClientBuilder; + private final SimpleHttpRequest request; + + private RequestConfig.Builder requestConfig; + + ApacheHttpRequest(HttpAsyncClientBuilder httpClientBuilder, SimpleHttpRequest request) { + this.httpClientBuilder = httpClientBuilder; + this.request = request; + // disable redirects as google-http-client handles redirects + this.requestConfig = + RequestConfig.custom() + .setRedirectsEnabled(false) + // TODO: enable set these somewhere down the call +// .setNormalizeUri(false) +// .setStaleConnectionCheckEnabled(false) + ; + } + + @Override + public void addHeader(String name, String value) { + request.addHeader(name, value); + } + + @Override + public void setTimeout(int connectTimeout, int readTimeout) throws IOException { + IOReactorConfig newConfig = IOReactorConfig.custom() + .setSoTimeout(Timeout.ofMilliseconds(readTimeout)) + .build(); + requestConfig.setConnectTimeout(Timeout.ofMilliseconds(connectTimeout)); + httpClientBuilder.setIOReactorConfig(newConfig); + } + + @Override + public LowLevelHttpResponse execute() throws IOException { + ApacheHttpRequestEntityProducer entityProducer = new ApacheHttpRequestEntityProducer(this); + + request.setConfig(requestConfig.build()); + final CompletableFuture responseFuture = new CompletableFuture<>(); + try { + CloseableHttpAsyncClient client = httpClientBuilder.build(); + client.start(); + client.execute( + new BasicRequestProducer(request, entityProducer), + SimpleResponseConsumer.create(), + new FutureCallback() { + @Override + public void completed(final SimpleHttpResponse response) { + responseFuture.complete(response); + } + + @Override + public void failed(final Exception exception) { + responseFuture.completeExceptionally(exception); + } + + @Override + public void cancelled() { + responseFuture.cancel(false); + } + } + ); + final SimpleHttpResponse response = responseFuture.get(); + return new ApacheHttpResponse(request, response); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + throw new IOException("Error making request", e); + } + } + +} diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequestEntity.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequestEntity.java new file mode 100644 index 000000000..bcc8d7d59 --- /dev/null +++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpRequestEntity.java @@ -0,0 +1,102 @@ +package com.google.api.client.http.apache.v3; + + +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Set; + +class ApacheHttpRequestEntityProducer implements AsyncEntityProducer { + private ApacheHttpRequest request; + + private ApacheHttpRequestEntityProducer() {} + + public ApacheHttpRequestEntityProducer(ApacheHttpRequest request) { + this.request = request; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public void failed(Exception e) { + // noop + } + + + @Override + public long getContentLength() { + return request.getContentLength(); + } + + @Override + public String getContentType() { + return request.getContentType(); + } + + @Override + public String getContentEncoding() { + return request.getContentEncoding(); + } + + @Override + public boolean isChunked() { + return false; + } + + @Override + public Set getTrailerNames() { + return Collections.emptySet(); + } + + /** + * Borrowed from {@link BasicAsyncEntityProducer#available()} + * @return + */ + @Override + public int available() { + return Integer.MAX_VALUE; + } + + @Override + public void produce(DataStreamChannel dataStreamChannel) throws IOException { + OutputStream dataStreamOutputStream = new OutputStream() { + private final java.nio.ByteBuffer buffer = ByteBuffer.allocate(1000); + + @Override + public void write(int b) throws IOException { + if (buffer.remaining() == 0) { + flush(); + } + buffer.put((byte) b); + } + + @Override + public void flush() throws IOException { + buffer.flip(); + dataStreamChannel.write(buffer); + buffer.compact(); + } + + @Override + public void close() throws IOException { + flush(); + dataStreamChannel.endStream(); + } + }; + request.getStreamingContent().writeTo(dataStreamOutputStream); + dataStreamOutputStream.close(); + } + + @Override + public void releaseResources() { + // no-op + } +} diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpResponse.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpResponse.java new file mode 100644 index 000000000..bcffcc055 --- /dev/null +++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpResponse.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.api.client.http.apache.v3; + +import com.google.api.client.http.LowLevelHttpResponse; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.message.StatusLine; + +final class ApacheHttpResponse extends LowLevelHttpResponse { + + private final HttpRequest request; + private final SimpleHttpResponse response; + private final Header[] allHeaders; + + ApacheHttpResponse(HttpRequest request, SimpleHttpResponse response) { + this.request = request; + this.response = response; + allHeaders = response.getHeaders(); + } + + @Override + public int getStatusCode() { + StatusLine statusLine = new StatusLine(response); + return statusLine == null ? 0 : statusLine.getStatusCode(); + } + + @Override + public InputStream getContent() throws IOException { + return new ByteArrayInputStream(response.getBodyBytes()); + } + + @Override + public String getContentEncoding() { + return response.getFirstHeader("Content-Encoding").getValue(); + } + + @Override + public long getContentLength() { + return response.getBodyText().length(); + } + + @Override + public String getContentType() { + return response.getContentType().toString(); + } + + @Override + public String getReasonPhrase() { + return response.getReasonPhrase(); + } + + @Override + public String getStatusLine() { + return new StatusLine(response).toString(); + } + + @Override + public int getHeaderCount() { + return allHeaders.length; + } + + @Override + public String getHeaderName(int index) { + return allHeaders[index].getName(); + } + + @Override + public String getHeaderValue(int index) { + return allHeaders[index].getValue(); + } +} diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpTransport.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpTransport.java new file mode 100644 index 000000000..d96d2ea3b --- /dev/null +++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/ApacheHttpTransport.java @@ -0,0 +1,225 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.api.client.http.apache.v3; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.util.Beta; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.config.TlsConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.core5.http.config.Http1Config; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.reactor.IOReactorConfig; + +import java.io.IOException; +import java.net.ProxySelector; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +/** + * Thread-safe HTTP transport based on the Apache HTTP Client library. + * + *

Implementation is thread-safe, as long as any parameter modification to the {@link + * #getHttpClient() Apache HTTP Client} is only done at initialization time. For maximum efficiency, + * applications should use a single globally-shared instance of the HTTP transport. + * + *

Default settings are specified in {@link #newDefaultHttpClient()}. Use the {@link + * #ApacheHttpTransport(CloseableHttpAsyncClient)} constructor to override the Apache HTTP Client used. Please + * read the + * Apache HTTP Client connection management tutorial for more complex configuration options. + * + * @author Yaniv Inbar + * @since 1.30 + */ +public final class ApacheHttpTransport extends HttpTransport { + + /** + * Apache HTTP client. + */ + private final HttpAsyncClientBuilder httpClientBuilder; + + /** + * If the HTTP client uses mTLS channel. + */ + private final boolean isMtls; + + /** + * Constructor that uses {@link #newDefaultHttpClient()} for the Apache HTTP client. + * + * @since 1.30 + */ + public ApacheHttpTransport() { + this(newDefaultHttpClientBuilder(), false); + } + + /** + * Constructor that allows an alternative Apache HTTP client to be used. + * + *

Note that in the previous version, we overrode several settings. However, we are no longer + * able to do so. + * + *

If you choose to provide your own Apache HttpClient implementation, be sure that + * + *

    + *
  • HTTP version is set to 1.1. + *
  • Redirects are disabled (google-http-client handles redirects). + *
  • Retries are disabled (google-http-client handles retries). + *
+ * + * @param httpClientBuilder Apache HTTP client to use + * @since 1.30 + */ + public ApacheHttpTransport(HttpAsyncClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + this.isMtls = false; + } + + /** + * {@link Beta}
+ * Constructor that allows an alternative Apache HTTP client to be used. + * + *

Note that in the previous version, we overrode several settings. However, we are no longer + * able to do so. + * + *

If you choose to provide your own Apache HttpClient implementation, be sure that + * + *

    + *
  • HTTP version is set to 1.1. + *
  • Redirects are disabled (google-http-client handles redirects). + *
  • Retries are disabled (google-http-client handles retries). + *
+ * + * @param httpClientBuilder Apache HTTP client to use + * @param isMtls If the HTTP client is mutual TLS + * @since 1.38 + */ + @Beta + public ApacheHttpTransport(HttpAsyncClientBuilder httpClientBuilder, boolean isMtls) { + this.httpClientBuilder = httpClientBuilder; + this.isMtls = isMtls; + } + + /** + * Creates a new instance of the Apache HTTP client that is used by the {@link + * #ApacheHttpTransport()} constructor. + * + *

Settings: + * + *

    + *
  • The client connection manager is set to {@link PoolingHttpClientConnectionManager}. + *
  • The route planner uses {@link SystemDefaultRoutePlanner} with {@link + * ProxySelector#getDefault()}, which uses the proxy settings from system + * properties. + *
+ * + * @return new instance of the Apache HTTP client + * @since 1.30 + */ + public static CloseableHttpAsyncClient newDefaultHttpClient() { + return newDefaultHttpClientBuilder().build(); + } + + /** + * Creates a new Apache HTTP client builder that is used by the {@link #ApacheHttpTransport()} + * constructor. + * + *

Settings: + * + *

    + *
  • The client connection manager is set to {@link PoolingHttpClientConnectionManager}. + *
  • The route planner uses {@link SystemDefaultRoutePlanner} with {@link + * ProxySelector#getDefault()}, which uses the proxy settings from system + * properties. + *
+ * + * @return new instance of the Apache HTTP client + * @since 1.31 + */ + public static HttpAsyncClientBuilder newDefaultHttpClientBuilder() { + + ConnectionConfig connectionConfig = ConnectionConfig.custom() + .setTimeToLive(-1, TimeUnit.MILLISECONDS) + .build(); + PoolingAsyncClientConnectionManager connectionManager = new PoolingAsyncClientConnectionManager(); + connectionManager.setMaxTotal(200); + connectionManager.setDefaultMaxPerRoute(20); + connectionManager.setDefaultConnectionConfig(connectionConfig); + connectionManager.setDefaultTlsConfig(TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2).build()); + + + return HttpAsyncClientBuilder.create() + .setH2Config(H2Config.DEFAULT) + .setHttp1Config(Http1Config.DEFAULT) + .setIOReactorConfig(IOReactorConfig.custom().setIoThreadCount(20).build()) + .setConnectionManager(connectionManager) + // socket factories are not configurable in the async client + //.setSSLSocketFactory(SSLConnectionSocketFactory.getSocketFactory()) + .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault())) + .disableRedirectHandling() + .disableAutomaticRetries(); + } + + @Override + public boolean supportsMethod(String method) { + return true; + } + + @Override + protected ApacheHttpRequest buildRequest(String method, String url) { + SimpleHttpRequest request = new SimpleHttpRequest(method, URI.create(url)); + return new ApacheHttpRequest(httpClientBuilder, request); + } + + /** + * Shuts down the connection manager and releases allocated resources. This closes all + * connections, whether they are currently used or not. + * + * @since 1.30 + */ + @Override + public void shutdown() throws IOException { + // no-op: we create short-lived clients in the requests + } + + /** + * Returns the Apache HTTP client. + * + * @since 1.30 + */ + public HttpAsyncClientBuilder getHttpClientBuilder() { + return httpClientBuilder; + } + + /** + * Returns if the underlying HTTP client is mTLS. + */ + @Override + public boolean isMtls() { + return isMtls; + } +} diff --git a/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/package-info.java b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/package-info.java new file mode 100644 index 000000000..0ff3f6518 --- /dev/null +++ b/google-http-client-apache-v3/src/main/java/com/google/api/client/http/apache/v3/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * HTTP Transport library for Google API's based on Apache HTTP Client version 4.5+. + * + * @since 1.30 + * @author Yaniv Inbar + */ +package com.google.api.client.http.apache.v3; diff --git a/google-http-client-apache-v3/src/main/resources/META-INF/native-image/com.google.http-client/google-http-client-apache-v2/reflect-config.json b/google-http-client-apache-v3/src/main/resources/META-INF/native-image/com.google.http-client/google-http-client-apache-v2/reflect-config.json new file mode 100644 index 000000000..97a9fba46 --- /dev/null +++ b/google-http-client-apache-v3/src/main/resources/META-INF/native-image/com.google.http-client/google-http-client-apache-v2/reflect-config.json @@ -0,0 +1,59 @@ +[ + { + "name": "org.apache.commons.logging.impl.LogFactoryImpl", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.apache.commons.logging.impl.Log4JLogger", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true + }, + { + "name": "org.apache.commons.logging.impl.Jdk14Logger", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true + }, + { + "name": "org.apache.commons.logging.impl.SimpleLog", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allPublicMethods": true, + "allDeclaredFields": true + }, + { + "name": "org.apache.commons.logging.impl.Jdk13LumberjackLogger", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true + }, + { + "name": "org.apache.commons.logging.LogFactory", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] \ No newline at end of file diff --git a/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpRequestTest.java b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpRequestTest.java new file mode 100644 index 000000000..983d74ebc --- /dev/null +++ b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpRequestTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.api.client.http.apache.v3; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.InputStreamContent; +import com.google.api.client.testing.http.apache.MockHttpClient; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.junit.Test; + +public class ApacheHttpRequestTest { + +// @Test +// public void testContentLengthSet() throws Exception { +// HttpExtensionMethod base = new HttpExtensionMethod("POST", "http://www.google.com"); +// ApacheHttpRequest request = new ApacheHttpRequest(new MockHttpClient(), base); +// HttpContent content = +// new ByteArrayContent("text/plain", "sample".getBytes(StandardCharsets.UTF_8)); +// request.setStreamingContent(content); +// request.setContentLength(content.getLength()); +// request.execute(); +// +// assertFalse(base.getEntity().isChunked()); +// assertEquals(6, base.getEntity().getContentLength()); +// } +// +// @Test +// public void testChunked() throws Exception { +// byte[] buf = new byte[300]; +// Arrays.fill(buf, (byte) ' '); +// HttpExtensionMethod base = new HttpExtensionMethod("POST", "http://www.google.com"); +// ApacheHttpRequest request = new ApacheHttpRequest(new MockHttpClient(), base); +// HttpContent content = new InputStreamContent("text/plain", new ByteArrayInputStream(buf)); +// request.setStreamingContent(content); +// request.execute(); +// +// assertTrue(base.getEntity().isChunked()); +// assertEquals(-1, base.getEntity().getContentLength()); +// } +} diff --git a/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpTransportTest.java b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpTransportTest.java new file mode 100644 index 000000000..5f1b5ba09 --- /dev/null +++ b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/ApacheHttpTransportTest.java @@ -0,0 +1,335 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.api.client.http.apache.v3; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.apache.MockHttpClient; +import com.google.api.client.util.ByteArrayStreamingContent; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.impl.io.HttpRequestExecutor; +import org.apache.hc.core5.http.io.HttpClientConnection; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests {@link ApacheHttpTransport}. + * + * @author Yaniv Inbar + */ +public class ApacheHttpTransportTest { + + private static class MockHttpResponse extends BasicHttpResponse { + public MockHttpResponse() { + super(200, "OK"); + } + } + + @Test + public void testApacheHttpTransport() { + ApacheHttpTransport transport = new ApacheHttpTransport(); + checkHttpTransport(transport); + assertFalse(transport.isMtls()); + } + + @Test + public void testApacheHttpTransportWithParam() { + ApacheHttpTransport transport = new ApacheHttpTransport(HttpAsyncClientBuilder.create(), true); + checkHttpTransport(transport); + assertTrue(transport.isMtls()); + } + + @Test + public void testNewDefaultHttpClient() { + HttpAsyncClientBuilder client = ApacheHttpTransport.newDefaultHttpClientBuilder(); + checkHttpClient(client.build()); + } + + private void checkHttpTransport(ApacheHttpTransport transport) { + assertNotNull(transport); + HttpAsyncClientBuilder clientBuilder = transport.getHttpClientBuilder(); + checkHttpClient(clientBuilder.build()); + } + + private void checkHttpClient(CloseableHttpAsyncClient client) { + assertNotNull(client); + // TODO(chingor): Is it possible to test this effectively? The newer HttpClient implementations + // are read-only and we're testing that we built the client with the right configuration + } + +// @Test +// public void testRequestsWithContent() throws IOException { +// HttpClient mockClient = +// new MockHttpClient() { +// @Override +// public CloseableHttpResponse execute(HttpUriRequest request) +// throws IOException, ClientProtocolException { +// return new MockHttpResponse(); +// } +// }; +// ApacheHttpTransport transport = new ApacheHttpTransport(mockClient); +// +// // Test GET. +// subtestUnsupportedRequestsWithContent( +// transport.buildRequest("GET", "http://www.test.url"), "GET"); +// // Test DELETE. +// subtestUnsupportedRequestsWithContent( +// transport.buildRequest("DELETE", "http://www.test.url"), "DELETE"); +// // Test HEAD. +// subtestUnsupportedRequestsWithContent( +// transport.buildRequest("HEAD", "http://www.test.url"), "HEAD"); +// +// // Test PATCH. +// execute(transport.buildRequest("PATCH", "http://www.test.url")); +// // Test PUT. +// execute(transport.buildRequest("PUT", "http://www.test.url")); +// // Test POST. +// execute(transport.buildRequest("POST", "http://www.test.url")); +// // Test PATCH. +// execute(transport.buildRequest("PATCH", "http://www.test.url")); +// } + +// private void subtestUnsupportedRequestsWithContent(ApacheHttpRequest request, String method) +// throws IOException { +// try { +// execute(request); +// fail("expected " + IllegalStateException.class); +// } catch (IllegalStateException e) { +// // expected +// assertEquals( +// e.getMessage(), +// "Apache HTTP client does not support " + method + " requests with content."); +// } +// } + + private void execute(ApacheHttpRequest request) throws IOException { + byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8); + request.setStreamingContent(new ByteArrayStreamingContent(bytes)); + request.setContentType("text/html"); + request.setContentLength(bytes.length); + request.execute(); + } + +// @Test +// public void testRequestShouldNotFollowRedirects() throws IOException { +// final AtomicInteger requestsAttempted = new AtomicInteger(0); +// HttpRequestExecutor requestExecutor = +// new HttpRequestExecutor() { +// +// @Override +// public ClassicHttpResponse execute( +// ClassicHttpRequest request, HttpClientConnection connection, HttpContext context) +// throws IOException, HttpException { +// ClassicHttpResponse response = new BasicHttpResponse(302); +// response.addHeader("location", "https://google.com/path"); +// requestsAttempted.incrementAndGet(); +// return response; +// } +// }; +// CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create().setRequest(requestExecutor).build(); +// ApacheHttpTransport transport = new ApacheHttpTransport(client); +// ApacheHttpRequest request = transport.buildRequest("GET", "https://google.com"); +// LowLevelHttpResponse response = request.execute(); +// assertEquals(1, requestsAttempted.get()); +// assertEquals(302, response.getStatusCode()); +// } + +// @Test +// public void testRequestCanSetHeaders() { +// final AtomicBoolean interceptorCalled = new AtomicBoolean(false); +// HttpClient client = +// HttpClients.custom() +// .addInterceptorFirst( +// new HttpRequestInterceptor() { +// @Override +// public void process(HttpRequest request, HttpContext context) +// throws HttpException, IOException { +// Header header = request.getFirstHeader("foo"); +// assertNotNull("Should have found header", header); +// assertEquals("bar", header.getValue()); +// interceptorCalled.set(true); +// throw new IOException("cancelling request"); +// } +// }) +// .build(); +// +// ApacheHttpTransport transport = new ApacheHttpTransport(client); +// ApacheHttpRequest request = transport.buildRequest("GET", "https://google.com"); +// request.addHeader("foo", "bar"); +// try { +// LowLevelHttpResponse response = request.execute(); +// fail("should not actually make the request"); +// } catch (IOException exception) { +// assertEquals("cancelling request", exception.getMessage()); +// } +// assertTrue("Expected to have called our test interceptor", interceptorCalled.get()); +// } + +// @Test(timeout = 10_000L) +// public void testConnectTimeout() { +// // Apache HttpClient doesn't appear to behave correctly on windows +// assumeFalse(isWindows()); +// // TODO(chanseok): Java 17 returns an IOException (SocketException: Network is unreachable). +// // Figure out a way to verify connection timeout works on Java 17+. +// assumeTrue(System.getProperty("java.version").compareTo("17") < 0); +// +// HttpTransport httpTransport = new ApacheHttpTransport(); +// GenericUrl url = new GenericUrl("http://google.com:81"); +// try { +// httpTransport.createRequestFactory().buildGetRequest(url).setConnectTimeout(100).execute(); +// fail("should have thrown an exception"); +// } catch (HttpHostConnectException | ConnectTimeoutException expected) { +// // expected +// } catch (IOException e) { +// fail("unexpected IOException: " + e.getClass().getName() + ": " + e.getMessage()); +// } +// } +// +// private static class FakeServer implements AutoCloseable { +// private final HttpServer server; +// private final ExecutorService executorService; +// +// FakeServer(HttpHandler httpHandler) throws IOException { +// server = HttpServer.create(new InetSocketAddress(0), 0); +// executorService = Executors.newFixedThreadPool(1); +// server.setExecutor(executorService); +// server.createContext("/", httpHandler); +// server.start(); +// } +// +// public int getPort() { +// return server.getAddress().getPort(); +// } +// +// @Override +// public void close() { +// server.stop(0); +// executorService.shutdownNow(); +// } +// } +// +// @Test +// public void testNormalizedUrl() throws IOException { +// final HttpHandler handler = +// new HttpHandler() { +// @Override +// public void handle(HttpExchange httpExchange) throws IOException { +// byte[] response = httpExchange.getRequestURI().toString().getBytes(); +// httpExchange.sendResponseHeaders(200, response.length); +// try (OutputStream out = httpExchange.getResponseBody()) { +// out.write(response); +// } +// } +// }; +// try (FakeServer server = new FakeServer(handler)) { +// HttpTransport transport = new ApacheHttpTransport(); +// GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); +// testUrl.setPort(server.getPort()); +// com.google.api.client.http.HttpResponse response = +// transport.createRequestFactory().buildGetRequest(testUrl).execute(); +// assertEquals(200, response.getStatusCode()); +// assertEquals("/foo//bar", response.parseAsString()); +// } +// } +// +// @Test +// public void testReadErrorStream() throws IOException { +// final HttpHandler handler = +// new HttpHandler() { +// @Override +// public void handle(HttpExchange httpExchange) throws IOException { +// byte[] response = "Forbidden".getBytes(StandardCharsets.UTF_8); +// httpExchange.sendResponseHeaders(403, response.length); +// try (OutputStream out = httpExchange.getResponseBody()) { +// out.write(response); +// } +// } +// }; +// try (FakeServer server = new FakeServer(handler)) { +// HttpTransport transport = new ApacheHttpTransport(); +// GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); +// testUrl.setPort(server.getPort()); +// com.google.api.client.http.HttpRequest getRequest = +// transport.createRequestFactory().buildGetRequest(testUrl); +// getRequest.setThrowExceptionOnExecuteError(false); +// com.google.api.client.http.HttpResponse response = getRequest.execute(); +// assertEquals(403, response.getStatusCode()); +// assertEquals("Forbidden", response.parseAsString()); +// } +// } +// +// @Test +// public void testReadErrorStream_withException() throws IOException { +// final HttpHandler handler = +// new HttpHandler() { +// @Override +// public void handle(HttpExchange httpExchange) throws IOException { +// byte[] response = "Forbidden".getBytes(StandardCharsets.UTF_8); +// httpExchange.sendResponseHeaders(403, response.length); +// try (OutputStream out = httpExchange.getResponseBody()) { +// out.write(response); +// } +// } +// }; +// try (FakeServer server = new FakeServer(handler)) { +// HttpTransport transport = new ApacheHttpTransport(); +// GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); +// testUrl.setPort(server.getPort()); +// com.google.api.client.http.HttpRequest getRequest = +// transport.createRequestFactory().buildGetRequest(testUrl); +// try { +// getRequest.execute(); +// Assert.fail(); +// } catch (HttpResponseException ex) { +// assertEquals("Forbidden", ex.getContent()); +// } +// } +// } +// +// private boolean isWindows() { +// return System.getProperty("os.name").startsWith("Windows"); +// } +} diff --git a/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/FirebaseTest.java b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/FirebaseTest.java new file mode 100644 index 000000000..6df8be22d --- /dev/null +++ b/google-http-client-apache-v3/src/test/java/com/google/api/client/http/apache/v3/FirebaseTest.java @@ -0,0 +1,148 @@ +package com.google.api.client.http.apache.v3; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class FirebaseTest { + public static FirebaseApp setup_admin_apache_http2_v3() throws IOException { + FileInputStream serviceAccount = new FileInputStream("src/main/resources/cert.json"); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .setProjectId("spring-autoconf-test") + .setHttpTransport(new ApacheHttp2Transport(true)) + .build(); + + return FirebaseApp.initializeApp(options); + } + + public static FirebaseApp setup_admin_apache_http_v3() throws IOException { + FileInputStream serviceAccount = new FileInputStream("src/main/resources/cert.json"); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .setProjectId("spring-autoconf-test") + .setHttpTransport(new ApacheHttpTransport()) + .build(); + + return FirebaseApp.initializeApp(options); + } + + public static List get_messages(int message_count) { + List messages = new ArrayList<>(message_count); + for (int i = 0; i < message_count; i++) { + Message message = Message.builder() + .setTopic(String.format("foo-bar-%d", i % 10)) + .build(); + messages.add(message); + } + return messages; + } + + public static void benchmark_send_each(List messages, int numRequests, FirebaseApp app) throws FileNotFoundException, IOException, FirebaseMessagingException { + long startTime = System.currentTimeMillis(); + for (int i = 0; i < numRequests; i++) { + BatchResponse response = FirebaseMessaging.getInstance(app).sendEach(messages, true); + System.out.println("Dry Run Response: " + response.getSuccessCount()); + } + + long endTime = System.currentTimeMillis(); + long totalTime = endTime - startTime; + double averageTime = (double) totalTime / numRequests; + + System.out.println("Total time: " + totalTime + " ms"); + System.out.println("Average time per request: " + averageTime + " ms"); + } + + + public static void benchmark_send_each_async(List messages, int numRequests, FirebaseApp app) throws FileNotFoundException, IOException, FirebaseMessagingException { + long startTime = System.currentTimeMillis(); + List> responseFutures = new ArrayList<>(); + + // Make request futures + for (int i = 0; i < numRequests; i++) { + responseFutures.add(FirebaseMessaging.getInstance(app).sendEachAsync(messages, true)); + } + + // Resolve All + try { + List responses = ApiFutures.allAsList(responseFutures).get(); + for (BatchResponse batchResponse : responses) { + System.out.println("Dry Run Response: " + batchResponse.getSuccessCount()); + } + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + throw new IOException("Error making request", e); + } + + long endTime = System.currentTimeMillis(); + long totalTime = endTime - startTime; + double averageTime = (double) totalTime / numRequests; + + System.out.println("Total time: " + totalTime + " ms"); + System.out.println("Average time per request: " + averageTime + " ms"); + } + + @SuppressWarnings("deprecation") + public static void benchmark_send_all(List messages, int numRequests, FirebaseApp app) throws FileNotFoundException, IOException, FirebaseMessagingException { + System.out.println("\nsendAll()"); + for (int i = 0; i < numRequests; i++) { + FirebaseMessaging.getInstance().sendEach(messages, true); + } + + long startTime = System.currentTimeMillis(); + for (int i = 0; i < numRequests; i++) { + BatchResponse response = FirebaseMessaging.getInstance(app).sendAll(messages, true); + System.out.println("Dry Run Response: " + response.getSuccessCount()); + } + + long endTime = System.currentTimeMillis(); + long totalTime = endTime - startTime; + double averageTime = (double) totalTime / numRequests; + + System.out.println("Total time: " + totalTime + " ms"); + System.out.println("Average time per request: " + averageTime + " ms"); + + // app.delete(); + } + + @Test + public void testFireBase() throws FirebaseMessagingException, IOException, InterruptedException { + + + List messages = get_messages(99); + int numRequests = 50; // Number of time to loop + FirebaseApp app; + + System.out.println("Start"); + app = setup_admin_apache_http2_v3(); +// app = setup_admin_apache_http_v3(); + benchmark_send_each(messages, numRequests, app); + benchmark_send_each_async(messages, numRequests, app); + benchmark_send_all(messages, numRequests, app); + + System.out.println("Sleep"); + TimeUnit.SECONDS.sleep(5); + System.out.println("Awake"); + + app.delete(); + + } + +} diff --git a/pom.xml b/pom.xml index 258e85db2..4f013a2e5 100644 --- a/pom.xml +++ b/pom.xml @@ -289,8 +289,8 @@ maven-compiler-plugin 3.13.0 - 1.7 - 1.7 + 1.8 + 1.8 @@ -431,7 +431,7 @@ maven-javadoc-plugin none - 7 + 8 diff --git a/versions.txt b/versions.txt index 963efeb8d..2f22e3a1c 100644 --- a/versions.txt +++ b/versions.txt @@ -7,6 +7,7 @@ google-http-client-parent:1.44.2:1.44.3-SNAPSHOT google-http-client-android:1.44.2:1.44.3-SNAPSHOT google-http-client-android-test:1.44.2:1.44.3-SNAPSHOT google-http-client-apache-v2:1.44.2:1.44.3-SNAPSHOT +google-http-client-apache-v3:1.44.2:1.44.3-SNAPSHOT google-http-client-appengine:1.44.2:1.44.3-SNAPSHOT google-http-client-assembly:1.44.2:1.44.3-SNAPSHOT google-http-client-findbugs:1.44.2:1.44.3-SNAPSHOT