Skip to content

Commit 2d30f78

Browse files
authored
fix: add nio entry to user-agent (#774)
Introduce StorageOptionsUtil which provides utilities and default instance access for StorageOptions which include our new user-agent entry. To all tests which called com.google.cloud.storage.contrib.nio.CloudStorageFileSystemProvider.setStorageOptions in their @before, there is now a corresponding @after which sets the storageOptions back to defaults
1 parent 63e38eb commit 2d30f78

14 files changed

+304
-31
lines changed

google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public final class CloudStorageFileSystemProvider extends FileSystemProvider {
9393
private final @Nullable String userProject;
9494

9595
// used only when we create a new instance of CloudStorageFileSystemProvider.
96-
private static StorageOptions futureStorageOptions;
96+
private static StorageOptions futureStorageOptions = StorageOptionsUtil.getDefaultInstance();
9797

9898
private static class LazyPathIterator extends AbstractIterator<Path> {
9999
private final Iterator<Blob> blobIterator;
@@ -198,20 +198,18 @@ public CloudStorageFileSystemProvider() {
198198
*/
199199
CloudStorageFileSystemProvider(
200200
@Nullable String userProject, @Nullable StorageOptions gcsStorageOptions) {
201-
this.storageOptions = gcsStorageOptions;
201+
this.storageOptions =
202+
gcsStorageOptions != null
203+
? StorageOptionsUtil.mergeOptionsWithUserAgent(gcsStorageOptions)
204+
: StorageOptionsUtil.getDefaultInstance();
202205
this.userProject = userProject;
203206
}
204207

205208
// Initialize this.storage, once. This may throw an exception if default authentication
206209
// credentials are not available (hence not doing it in the ctor).
207210
private void initStorage() {
208-
if (this.storage != null) {
209-
return;
210-
}
211-
if (storageOptions == null) {
212-
this.storage = StorageOptions.getDefaultInstance().getService();
213-
} else {
214-
this.storage = storageOptions.getService();
211+
if (this.storage == null) {
212+
doInitStorage();
215213
}
216214
}
217215

@@ -1037,4 +1035,9 @@ private IOException asIoException(StorageException oops) {
10371035
}
10381036
return new IOException(oops.getMessage(), oops);
10391037
}
1038+
1039+
@VisibleForTesting
1040+
void doInitStorage() {
1041+
this.storage = storageOptions.getService();
1042+
}
10401043
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.storage.contrib.nio;
18+
19+
import com.google.api.gax.rpc.FixedHeaderProvider;
20+
import com.google.api.gax.rpc.HeaderProvider;
21+
import com.google.cloud.storage.StorageOptions;
22+
import com.google.common.annotations.VisibleForTesting;
23+
import com.google.common.collect.ImmutableMap;
24+
import java.io.IOException;
25+
import java.io.InputStream;
26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.Map;
29+
import java.util.Properties;
30+
31+
final class StorageOptionsUtil {
32+
static final String USER_AGENT_ENTRY_NAME = "gcloud-java-nio";
33+
static final String USER_AGENT_ENTRY_VERSION = getVersion();
34+
private static final String USER_AGENT_ENTRY =
35+
String.format("%s/%s", USER_AGENT_ENTRY_NAME, USER_AGENT_ENTRY_VERSION);
36+
private static final FixedHeaderProvider DEFAULT_HEADER_PROVIDER =
37+
FixedHeaderProvider.create("user-agent", USER_AGENT_ENTRY);
38+
39+
private static final StorageOptions DEFAULT_STORAGE_OPTIONS_INSTANCE =
40+
StorageOptions.newBuilder().setHeaderProvider(DEFAULT_HEADER_PROVIDER).build();
41+
private static final FixedHeaderProvider EMTPY_HEADER_PROVIDER =
42+
FixedHeaderProvider.create(Collections.emptyMap());
43+
44+
private StorageOptionsUtil() {}
45+
46+
static StorageOptions getDefaultInstance() {
47+
return DEFAULT_STORAGE_OPTIONS_INSTANCE;
48+
}
49+
50+
static StorageOptions mergeOptionsWithUserAgent(StorageOptions providedStorageOptions) {
51+
if (providedStorageOptions == DEFAULT_STORAGE_OPTIONS_INSTANCE) {
52+
return providedStorageOptions;
53+
}
54+
55+
String userAgent = providedStorageOptions.getUserAgent();
56+
if (userAgent == null) {
57+
return nullSafeSet(providedStorageOptions, DEFAULT_HEADER_PROVIDER);
58+
} else {
59+
if (!userAgent.contains(USER_AGENT_ENTRY_NAME)) {
60+
HeaderProvider providedHeaderProvider = getHeaderProvider(providedStorageOptions);
61+
Map<String, String> newHeaders = new HashMap<>(providedHeaderProvider.getHeaders());
62+
newHeaders.put("user-agent", String.format("%s %s", userAgent, USER_AGENT_ENTRY));
63+
FixedHeaderProvider headerProvider =
64+
FixedHeaderProvider.create(ImmutableMap.copyOf(newHeaders));
65+
return nullSafeSet(providedStorageOptions, headerProvider);
66+
} else {
67+
return providedStorageOptions;
68+
}
69+
}
70+
}
71+
72+
/**
73+
* Due to some complex interactions between init and mocking, it's possible that the builder
74+
* instance returned from {@link StorageOptions#toBuilder()} can be null. This utility method will
75+
* attempt to create the builder and set the new header provider. If however the builder instance
76+
* is null, the orignal options will be returned without setting the header provider.
77+
*
78+
* <p>Since this method is only every called by us trying to add our user-agent entry to the
79+
* headers this makes our attempt effectively a no-op, which is much better than failing customer
80+
* code.
81+
*/
82+
private static StorageOptions nullSafeSet(
83+
StorageOptions storageOptions, HeaderProvider headerProvider) {
84+
StorageOptions.Builder builder = storageOptions.toBuilder();
85+
if (builder == null) {
86+
return storageOptions;
87+
} else {
88+
return builder.setHeaderProvider(headerProvider).build();
89+
}
90+
}
91+
92+
/** Resolve the version of google-cloud-nio for inclusion in request meta-data */
93+
private static String getVersion() {
94+
// attempt to read the library's version from a properties file generated during the build
95+
// this value should be read and cached for later use
96+
String version = "";
97+
try (InputStream inputStream =
98+
CloudStorageFileSystemProvider.class.getResourceAsStream(
99+
"/META-INF/maven/com.google.cloud/google-cloud-nio/pom.properties")) {
100+
if (inputStream != null) {
101+
final Properties properties = new Properties();
102+
properties.load(inputStream);
103+
version = properties.getProperty("version");
104+
}
105+
} catch (IOException e) {
106+
// ignore
107+
}
108+
return version;
109+
}
110+
111+
/**
112+
* {@link com.google.cloud.ServiceOptions} does not specify a getter for the headerProvider, so
113+
* instead merge with an empty provider.
114+
*/
115+
@VisibleForTesting
116+
static HeaderProvider getHeaderProvider(StorageOptions options) {
117+
return options.getMergedHeaderProvider(EMTPY_HEADER_PROVIDER);
118+
}
119+
}

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileAttributeViewTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.nio.file.Path;
3131
import java.nio.file.Paths;
3232
import java.nio.file.attribute.FileTime;
33+
import org.junit.After;
3334
import org.junit.Assert;
3435
import org.junit.Before;
3536
import org.junit.Rule;
@@ -52,6 +53,11 @@ public void before() {
5253
path = Paths.get(URI.create("gs://red/water"));
5354
}
5455

56+
@After
57+
public void after() {
58+
CloudStorageFileSystemProvider.setStorageOptions(StorageOptionsUtil.getDefaultInstance());
59+
}
60+
5561
@Test
5662
public void testReadAttributes() throws IOException {
5763
Files.write(path, HAPPY, CloudStorageOptions.withCacheControl("potato"));

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileAttributesTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.nio.file.Files;
3030
import java.nio.file.Path;
3131
import java.nio.file.Paths;
32+
import org.junit.After;
3233
import org.junit.Before;
3334
import org.junit.Rule;
3435
import org.junit.Test;
@@ -53,6 +54,11 @@ public void before() {
5354
dir = Paths.get(URI.create("gs://bucket/randompath/"));
5455
}
5556

57+
@After
58+
public void after() {
59+
CloudStorageFileSystemProvider.setStorageOptions(StorageOptionsUtil.getDefaultInstance());
60+
}
61+
5662
@Test
5763
public void testCacheControl() throws IOException {
5864
Files.write(path, HAPPY, CloudStorageOptions.withCacheControl("potato"));

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static java.nio.file.StandardOpenOption.CREATE_NEW;
2727
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
2828
import static java.nio.file.StandardOpenOption.WRITE;
29+
import static org.junit.Assert.assertTrue;
2930

3031
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
3132
import com.google.cloud.testing.junit4.MultipleAttemptsRule;
@@ -52,6 +53,7 @@
5253
import java.util.HashMap;
5354
import java.util.List;
5455
import java.util.Map;
56+
import org.junit.After;
5557
import org.junit.Assert;
5658
import org.junit.Before;
5759
import org.junit.Rule;
@@ -99,6 +101,11 @@ public void before() {
99101
CloudStorageFileSystemProvider.setStorageOptions(LocalStorageHelper.getOptions());
100102
}
101103

104+
@After
105+
public void after() {
106+
CloudStorageFileSystemProvider.setStorageOptions(StorageOptionsUtil.getDefaultInstance());
107+
}
108+
102109
@Test
103110
public void testSize() throws Exception {
104111
Path path = Paths.get(URI.create("gs://bucket/wat"));
@@ -795,6 +802,21 @@ public void testFromSpace() throws Exception {
795802
assertThat(path4.toString()).isEqualTo("/with/a%20percent");
796803
}
797804

805+
@Test
806+
public void testVersion_matchesAcceptablePatterns() {
807+
String acceptableVersionPattern = "|(?:\\d+\\.\\d+\\.\\d+(?:-.*?)?(?:-SNAPSHOT)?)";
808+
String version = StorageOptionsUtil.USER_AGENT_ENTRY_VERSION;
809+
assertTrue(
810+
String.format("the loaded version '%s' did not match the acceptable pattern", version),
811+
version.matches(acceptableVersionPattern));
812+
}
813+
814+
@Test
815+
public void getUserAgentStartsWithCorrectToken() {
816+
assertThat(String.format("gcloud-java-nio/%s", StorageOptionsUtil.USER_AGENT_ENTRY_VERSION))
817+
.startsWith("gcloud-java-nio/");
818+
}
819+
798820
private static CloudStorageConfiguration permitEmptyPathComponents(boolean value) {
799821
return CloudStorageConfiguration.builder().permitEmptyPathComponents(value).build();
800822
}

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import java.nio.file.attribute.BasicFileAttributes;
4747
import java.util.ArrayList;
4848
import java.util.List;
49+
import org.junit.After;
4950
import org.junit.Assert;
5051
import org.junit.Before;
5152
import org.junit.Rule;
@@ -73,6 +74,11 @@ public void before() {
7374
CloudStorageFileSystemProvider.setStorageOptions(LocalStorageHelper.getOptions());
7475
}
7576

77+
@After
78+
public void after() {
79+
CloudStorageFileSystemProvider.setStorageOptions(StorageOptionsUtil.getDefaultInstance());
80+
}
81+
7682
@Test
7783
public void checkDefaultOptions() throws IOException {
7884
// 1. We get the normal default if we don't do anything special.

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageIsDirectoryTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.cloud.testing.junit4.MultipleAttemptsRule;
3030
import com.google.common.collect.Lists;
3131
import java.nio.file.Files;
32+
import org.junit.After;
3233
import org.junit.Before;
3334
import org.junit.Rule;
3435
import org.junit.Test;
@@ -57,6 +58,11 @@ public void before() {
5758
CloudStorageFileSystemProvider.setStorageOptions(mockOptions);
5859
}
5960

61+
@After
62+
public void after() {
63+
CloudStorageFileSystemProvider.setStorageOptions(StorageOptionsUtil.getDefaultInstance());
64+
}
65+
6066
@Test
6167
public void testIsDirectoryNoUserProject() {
6268
CloudStorageFileSystem fs =

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageLateInitializationTest.java

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,56 +16,41 @@
1616

1717
package com.google.cloud.storage.contrib.nio;
1818

19-
import static org.mockito.Mockito.mock;
2019
import static org.mockito.Mockito.never;
2120
import static org.mockito.Mockito.times;
2221
import static org.mockito.Mockito.verify;
23-
import static org.mockito.Mockito.when;
2422

25-
import com.google.cloud.storage.Storage;
26-
import com.google.cloud.storage.StorageOptions;
2723
import com.google.cloud.testing.junit4.MultipleAttemptsRule;
2824
import java.net.URI;
29-
import org.junit.Before;
3025
import org.junit.Rule;
3126
import org.junit.Test;
3227
import org.junit.runner.RunWith;
33-
import org.junit.runners.JUnit4;
28+
import org.mockito.Spy;
29+
import org.mockito.junit.MockitoJUnitRunner;
3430

3531
/** Unit tests for {@link CloudStorageFileSystemProvider} late initialization. */
36-
@RunWith(JUnit4.class)
32+
@RunWith(MockitoJUnitRunner.class)
3733
public class CloudStorageLateInitializationTest {
3834
@Rule public final MultipleAttemptsRule multipleAttemptsRule = new MultipleAttemptsRule(3);
3935

40-
private StorageOptions mockOptions;
41-
42-
@Before
43-
public void before() {
44-
mockOptions = mock(StorageOptions.class);
45-
Storage mockStorage = mock(Storage.class);
46-
when(mockOptions.getService()).thenReturn(mockStorage);
47-
CloudStorageFileSystemProvider.setStorageOptions(mockOptions);
48-
}
36+
@Spy private final CloudStorageFileSystemProvider provider = new CloudStorageFileSystemProvider();
4937

5038
@Test
5139
public void ctorDoesNotCreateStorage() {
52-
new CloudStorageFileSystemProvider();
53-
verify(mockOptions, never()).getService();
40+
verify(provider, never()).doInitStorage();
5441
}
5542

5643
@Test
5744
public void getPathCreatesStorageOnce() {
58-
CloudStorageFileSystemProvider provider = new CloudStorageFileSystemProvider();
5945
provider.getPath(URI.create("gs://bucket1/wat"));
6046
provider.getPath(URI.create("gs://bucket2/wat"));
61-
verify(mockOptions, times(1)).getService();
47+
verify(provider, times(1)).doInitStorage();
6248
}
6349

6450
@Test
6551
public void getFileSystemCreatesStorageOnce() {
66-
CloudStorageFileSystemProvider provider = new CloudStorageFileSystemProvider();
6752
provider.getFileSystem(URI.create("gs://bucket1"));
6853
provider.getFileSystem(URI.create("gs://bucket2"));
69-
verify(mockOptions, times(1)).getService();
54+
verify(provider, times(1)).doInitStorage();
7055
}
7156
}

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageOptionsTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.nio.file.Files;
2929
import java.nio.file.Path;
3030
import java.nio.file.Paths;
31+
import org.junit.After;
3132
import org.junit.Before;
3233
import org.junit.Rule;
3334
import org.junit.Test;
@@ -44,6 +45,11 @@ public void before() {
4445
CloudStorageFileSystemProvider.setStorageOptions(LocalStorageHelper.getOptions());
4546
}
4647

48+
@After
49+
public void after() {
50+
CloudStorageFileSystemProvider.setStorageOptions(StorageOptionsUtil.getDefaultInstance());
51+
}
52+
4753
@Test
4854
public void testWithoutCaching() throws IOException {
4955
Path path = Paths.get(URI.create("gs://bucket/path"));

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStoragePathTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.nio.file.FileSystems;
3131
import java.nio.file.Path;
3232
import java.nio.file.ProviderMismatchException;
33+
import org.junit.After;
3334
import org.junit.Assert;
3435
import org.junit.Before;
3536
import org.junit.Rule;
@@ -47,6 +48,11 @@ public void before() {
4748
CloudStorageFileSystemProvider.setStorageOptions(LocalStorageHelper.getOptions());
4849
}
4950

51+
@After
52+
public void after() {
53+
CloudStorageFileSystemProvider.setStorageOptions(StorageOptionsUtil.getDefaultInstance());
54+
}
55+
5056
@Test
5157
public void testCreate_neverRemoveExtraSlashes() throws IOException {
5258
try (CloudStorageFileSystem fs = CloudStorageFileSystem.forBucket("doodle")) {

0 commit comments

Comments
 (0)