diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/GaxHttpJsonProperties.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/GaxHttpJsonProperties.java index fab63c320c..a12f771fdc 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/GaxHttpJsonProperties.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/GaxHttpJsonProperties.java @@ -36,7 +36,7 @@ @InternalApi public class GaxHttpJsonProperties { private static final Pattern DEFAULT_API_CLIENT_HEADER_PATTERN = - Pattern.compile("gl-java/.+ gapic/.* gax/.+ rest/.*"); + Pattern.compile("gl-java/.+ gapic/.*?--protobuf-.+ gax/.+ rest/.*"); /** Returns default api client header pattern (to facilitate testing) */ public static Pattern getDefaultApiClientHeaderPattern() { diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/GaxHttpJsonPropertiesTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/GaxHttpJsonPropertiesTest.java index 007b100577..2547ec95bd 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/GaxHttpJsonPropertiesTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/GaxHttpJsonPropertiesTest.java @@ -41,7 +41,7 @@ class GaxHttpJsonPropertiesTest { void testDefaultHeaderPattern() { assertTrue( GaxHttpJsonProperties.getDefaultApiClientHeaderPattern() - .matcher("gl-java/1.8_00 gapic/1.2.3-alpha gax/1.5.0 rest/1.7.0") + .matcher("gl-java/1.8_00 gapic/1.2.3-alpha--protobuf-1.5.0 gax/1.5.0 rest/1.7.0") .matches()); } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/core/GaxProperties.java b/gax-java/gax/src/main/java/com/google/api/gax/core/GaxProperties.java index 66fbaf887a..20d5ecc0dc 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/core/GaxProperties.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/core/GaxProperties.java @@ -32,9 +32,15 @@ import com.google.api.core.InternalApi; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.protobuf.Any; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.Optional; import java.util.Properties; +import java.util.jar.Attributes; +import java.util.jar.JarFile; /** Provides properties of the GAX library. */ @InternalApi @@ -43,6 +49,8 @@ public class GaxProperties { private static final String DEFAULT_VERSION = ""; private static final String GAX_VERSION = getLibraryVersion(GaxProperties.class, "version.gax"); private static final String JAVA_VERSION = getRuntimeVersion(); + private static final String PROTOBUF_VERSION = + getBundleVersion(Any.class).orElse(DEFAULT_VERSION); private GaxProperties() {} @@ -91,6 +99,11 @@ public static String getGaxVersion() { return GAX_VERSION; } + /** Returns the current version of protobuf runtime library. */ + public static String getProtobufVersion() { + return PROTOBUF_VERSION; + } + /** * Returns the current runtime version. For GraalVM the values in this method will be fetched at * build time and the values should not differ from the runtime (executable) @@ -113,4 +126,27 @@ static String getRuntimeVersion() { // with hyphens. return javaRuntimeInformation.replaceAll("[^0-9a-zA-Z_\\\\.]", "-"); } + + /** + * Returns the current library version as reported by Bundle-Version attribute in library's + * META-INF/MANIFEST for libraries using OSGi bundle manifest specification + * https://www.ibm.com/docs/en/wasdtfe?topic=overview-osgi-bundles. This should only be used if + * MANIFEST file does not contain a widely recognized version declaration such as Specific-Version + * OR Implementation-Version declared in Manifest Specification + * https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Manifest_Specification, + * otherwise please use #getLibraryVersion + */ + @VisibleForTesting + static Optional getBundleVersion(Class clazz) { + try { + File file = new File(clazz.getProtectionDomain().getCodeSource().getLocation().toURI()); + try (JarFile jar = new JarFile(file.getPath())) { + Attributes attributes = jar.getManifest().getMainAttributes(); + return Optional.ofNullable(attributes.getValue("Bundle-Version")); + } + } catch (URISyntaxException | IOException e) { + // Unable to read Bundle-Version from manifest. Recover gracefully. + return Optional.empty(); + } + } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java index a5e80e10b1..35307764d2 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java @@ -33,6 +33,8 @@ import com.google.common.collect.ImmutableMap; import java.io.Serializable; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Implementation of HeaderProvider that provides headers describing the API client library making @@ -41,6 +43,7 @@ public class ApiClientHeaderProvider implements HeaderProvider, Serializable { private static final long serialVersionUID = -8876627296793342119L; static final String QUOTA_PROJECT_ID_HEADER_KEY = "x-goog-user-project"; + static final String PROTOBUF_HEADER_VERSION_KEY = "protobuf"; public static final String API_VERSION_HEADER_KEY = "x-goog-api-version"; @@ -57,8 +60,12 @@ protected ApiClientHeaderProvider(Builder builder) { appendToken(apiClientHeaderValue, builder.getGeneratedLibToken()); appendToken(apiClientHeaderValue, builder.getGeneratedRuntimeToken()); appendToken(apiClientHeaderValue, builder.getTransportToken()); + appendToken(apiClientHeaderValue, builder.protobufRuntimeToken); + if (apiClientHeaderValue.length() > 0) { - headersBuilder.put(builder.getApiClientHeaderKey(), apiClientHeaderValue.toString()); + headersBuilder.put( + builder.getApiClientHeaderKey(), + checkAndAppendProtobufVersionIfNecessary(apiClientHeaderValue)); } } @@ -76,6 +83,22 @@ protected ApiClientHeaderProvider(Builder builder) { this.headers = headersBuilder.build(); } + private static String checkAndAppendProtobufVersionIfNecessary( + StringBuilder apiClientHeaderValue) { + // TODO(b/366417603): appending protobuf version to existing client library token until resolved + Pattern pattern = Pattern.compile("(gccl|gapic)\\S*"); + Matcher matcher = pattern.matcher(apiClientHeaderValue); + if (matcher.find()) { + return apiClientHeaderValue.substring(0, matcher.end()) + + "--" + + PROTOBUF_HEADER_VERSION_KEY + + "-" + + GaxProperties.getProtobufVersion() + + apiClientHeaderValue.substring(matcher.end()); + } + return apiClientHeaderValue.toString(); + } + @Override public Map getHeaders() { return headers; @@ -110,6 +133,7 @@ public static class Builder { private String generatedRuntimeToken; private String transportToken; private String quotaProjectIdToken; + private final String protobufRuntimeToken; private String resourceHeaderKey; private String resourceToken; @@ -125,11 +149,11 @@ protected Builder() { setClientRuntimeToken(GaxProperties.getGaxVersion()); transportToken = null; quotaProjectIdToken = null; - resourceHeaderKey = getDefaultResourceHeaderKey(); resourceToken = null; - apiVersionToken = null; + protobufRuntimeToken = + constructToken(PROTOBUF_HEADER_VERSION_KEY, GaxProperties.getProtobufVersion()); } public String getApiClientHeaderKey() { diff --git a/gax-java/gax/src/test/java/com/google/api/gax/core/GaxPropertiesTest.java b/gax-java/gax/src/test/java/com/google/api/gax/core/GaxPropertiesTest.java index c27396c6b6..1369ec35ae 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/core/GaxPropertiesTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/core/GaxPropertiesTest.java @@ -29,10 +29,14 @@ */ package com.google.api.gax.core; +import static com.google.api.gax.core.GaxProperties.getBundleVersion; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.common.base.Strings; +import java.io.IOException; +import java.util.Optional; import java.util.regex.Pattern; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -41,17 +45,11 @@ class GaxPropertiesTest { @Test void testGaxVersion() { - String gaxVersion = GaxProperties.getGaxVersion(); - assertTrue(Pattern.compile("^\\d+\\.\\d+\\.\\d+").matcher(gaxVersion).find()); - String[] versionComponents = gaxVersion.split("\\."); - // This test was added in version 1.56.0, so check that the major and minor numbers are greater - // than that. - int major = Integer.parseInt(versionComponents[0]); - int minor = Integer.parseInt(versionComponents[1]); + Version version = readVersion(GaxProperties.getGaxVersion()); - assertTrue(major >= 1); - if (major == 1) { - assertTrue(minor >= 56); + assertTrue(version.major >= 1); + if (version.major == 1) { + assertTrue(version.minor >= 56); } } @@ -159,4 +157,41 @@ void testGetJavaRuntimeInfo_nullJavaVersion() { String runtimeInfo = GaxProperties.getRuntimeVersion(); assertEquals("null__oracle__20.0.1", runtimeInfo); } + + @Test + public void testGetProtobufVersion() throws IOException { + Version version = readVersion(GaxProperties.getProtobufVersion()); + + assertTrue(version.major >= 3); + if (version.major == 3) { + assertTrue(version.minor >= 25); + } + } + + @Test + public void testGetBundleVersion_noManifestFile() throws IOException { + Optional version = getBundleVersion(GaxProperties.class); + + assertFalse(version.isPresent()); + } + + private Version readVersion(String version) { + assertTrue(Pattern.compile("^\\d+\\.\\d+\\.\\d+").matcher(version).find()); + String[] versionComponents = version.split("\\."); + // This test was added in version 1.56.0, so check that the major and minor numbers are greater + // than that. + int major = Integer.parseInt(versionComponents[0]); + int minor = Integer.parseInt(versionComponents[1]); + return new Version(major, minor); + } + + private static class Version { + public int major; + public int minor; + + public Version(int major, int minor) { + this.major = major; + this.minor = minor; + } + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ApiClientHeaderProviderTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ApiClientHeaderProviderTest.java index baccb901c2..8dcf74fd8a 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ApiClientHeaderProviderTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ApiClientHeaderProviderTest.java @@ -42,7 +42,8 @@ class ApiClientHeaderProviderTest { void testServiceHeaderDefault() { ApiClientHeaderProvider provider = ApiClientHeaderProvider.newBuilder().build(); assertThat(provider.getHeaders().size()).isEqualTo(1); - assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)).matches("^gl-java/.* gax/.*$"); + assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) + .matches("^gl-java/.* gax/.* protobuf/.*"); } @Test @@ -51,7 +52,7 @@ void testServiceHeaderManual() { ApiClientHeaderProvider.newBuilder().setClientLibToken("gccl", "1.2.3").build(); assertThat(provider.getHeaders().size()).isEqualTo(1); assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) - .matches("^gl-java/.* gccl/1\\.2\\.3 gax/.*$"); + .matches("^gl-java/.* gccl/1\\.2\\.3--protobuf-.* gax/.* protobuf/.*"); } @Test @@ -64,7 +65,8 @@ void testServiceHeaderManualGapic() { .build(); assertThat(provider.getHeaders().size()).isEqualTo(1); assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) - .matches("^gl-java/.* gccl/4\\.5\\.6 gapic/7\\.8\\.9 gax/.* grpc/1\\.2\\.3$"); + .matches( + "^gl-java/.* gccl/4\\.5\\.6--protobuf-.* gapic/7\\.8\\.9 gax/.* grpc/1\\.2\\.3 protobuf/.*"); } @Test @@ -76,7 +78,7 @@ void testServiceHeaderManualGrpc() { .build(); assertThat(provider.getHeaders().size()).isEqualTo(1); assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) - .matches("^gl-java/.* gccl/4\\.5\\.6 gax/.* grpc/1\\.2\\.3$"); + .matches("^gl-java/.* gccl/4\\.5\\.6--protobuf-.* gax/.* grpc/1\\.2\\.3 protobuf/.*"); } @Test @@ -88,7 +90,7 @@ void testServiceHeaderGapic() { .build(); assertThat(provider.getHeaders().size()).isEqualTo(1); assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) - .matches("^gl-java/.* gapic/4\\.5\\.6 gax/.* grpc/1\\.2\\.3$"); + .matches("^gl-java/.* gapic/4\\.5\\.6--protobuf-.* gax/.* grpc/1\\.2\\.3 protobuf/.*"); } @Test @@ -101,7 +103,7 @@ void testCloudResourcePrefixHeader() { .build(); assertThat(provider.getHeaders().size()).isEqualTo(2); assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) - .matches("^gl-java/.* gapic/4\\.5\\.6 gax/.* grpc/1\\.2\\.3$"); + .matches("^gl-java/.* gapic/4\\.5\\.6--protobuf-.* gax/.* grpc/1\\.2\\.3 protobuf/.*"); assertThat(provider.getHeaders().get(CLOUD_RESOURCE_PREFIX)).isEqualTo("test-prefix"); } @@ -117,7 +119,7 @@ void testCustomHeaderKeys() { .build(); assertThat(provider.getHeaders().size()).isEqualTo(2); assertThat(provider.getHeaders().get("custom-header1")) - .matches("^gl-java/.* gapic/4\\.5\\.6 gax/.* grpc/1\\.2\\.3$"); + .matches("^gl-java/.* gapic/4\\.5\\.6--protobuf-.* gax/.* grpc/1\\.2\\.3 protobuf/.*"); assertThat(provider.getHeaders().get("custom-header2")).isEqualTo("test-prefix"); } @@ -131,7 +133,7 @@ void testQuotaProjectHeader() { .build(); assertThat(provider.getHeaders().size()).isEqualTo(2); assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) - .matches("^gl-java/.* gccl/1\\.2\\.3 gax/.*$"); + .matches("^gl-java/.* gccl/1\\.2\\.3--protobuf-.* gax/.* protobuf/.*"); assertThat(provider.getHeaders().get(ApiClientHeaderProvider.QUOTA_PROJECT_ID_HEADER_KEY)) .matches(quotaProjectHeaderValue); } @@ -149,4 +151,22 @@ void testApiVersionHeader() { assertThat( emptyProvider.getHeaders().get(ApiClientHeaderProvider.API_VERSION_HEADER_KEY).isEmpty()); } + + @Test + void testNonGapicGeneratedLibToken_doesNotAppendProtobufVersion() { + ApiClientHeaderProvider provider = + ApiClientHeaderProvider.newBuilder().setGeneratedLibToken("other-token", "1.2.3").build(); + + assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) + .matches("^gl-java/.* other-token/1.2.3 gax/.* protobuf/.*"); + } + + @Test + void testNonGcclGeneratedLibToken_doesNotAppendProtobufVersion() { + ApiClientHeaderProvider provider = + ApiClientHeaderProvider.newBuilder().setClientLibToken("other-token", "1.2.3").build(); + + assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)) + .matches("^gl-java/.* other-token/1.2.3 gax/.* protobuf/.*"); + } } diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITVersionHeaders.java similarity index 87% rename from showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java rename to showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITVersionHeaders.java index cb823db498..09255fe278 100644 --- a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITVersionHeaders.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.api.gax.httpjson.*; import com.google.api.gax.rpc.ApiClientHeaderProvider; @@ -33,6 +34,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -41,13 +43,19 @@ // https://github.com/googleapis/gapic-showcase/pull/1456 // TODO: watch for showcase gRPC trailer changes suggested in // https://github.com/googleapis/gapic-showcase/pull/1509#issuecomment-2089147103 -class ITApiVersionHeaders { +class ITVersionHeaders { private static final String HTTP_RESPONSE_HEADER_STRING = "x-showcase-request-" + ApiClientHeaderProvider.API_VERSION_HEADER_KEY; + private static final String HTTP_CLIENT_API_HEADER_KEY = + "x-showcase-request-" + ApiClientHeaderProvider.getDefaultApiClientHeaderKey(); private static final Metadata.Key API_VERSION_HEADER_KEY = Metadata.Key.of( ApiClientHeaderProvider.API_VERSION_HEADER_KEY, Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key API_CLIENT_HEADER_KEY = + Metadata.Key.of( + ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), Metadata.ASCII_STRING_MARSHALLER); + private static final String EXPECTED_ECHO_API_VERSION = "v1_20240408"; private static final String CUSTOM_API_VERSION = "user-supplied-version"; private static final String EXPECTED_EXCEPTION_MESSAGE = @@ -229,4 +237,25 @@ void testHttpJsonCompliance_userApiVersionSetSuccess() throws IOException { assertThat(headerValue).isEqualTo(CUSTOM_API_VERSION); } } + + @Test + void testGrpcCall_sendsCorrectApiClientHeader() { + Pattern defautlGrpcHeaderPattern = + Pattern.compile("gl-java/.* gapic/.*?--protobuf-.* gax/.* grpc/.* protobuf/.*"); + grpcClient.echo(EchoRequest.newBuilder().build()); + String headerValue = grpcInterceptor.metadata.get(API_CLIENT_HEADER_KEY); + assertTrue(defautlGrpcHeaderPattern.matcher(headerValue).matches()); + } + + @Test + void testHttpJson_sendsCorrectApiClientHeader() { + Pattern defautlHttpHeaderPattern = + Pattern.compile("gl-java/.* gapic/.*?--protobuf-.* gax/.* rest/ protobuf/.*"); + httpJsonClient.echo(EchoRequest.newBuilder().build()); + ArrayList headerValues = + (ArrayList) + httpJsonInterceptor.metadata.getHeaders().get(HTTP_CLIENT_API_HEADER_KEY); + String headerValue = headerValues.get(0); + assertTrue(defautlHttpHeaderPattern.matcher(headerValue).matches()); + } }