Skip to content

Commit 33e932e

Browse files
committed
Merge pull request #279 from mziccard/fix-unset-metadata
Provide a way to unset blob metadata
2 parents eef07a7 + 682848a commit 33e932e

File tree

4 files changed

+144
-45
lines changed

4 files changed

+144
-45
lines changed

gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ public Blob reload(BlobSourceOption... options) {
190190
* made on the metadata generation of the current blob. If you want to update the information only
191191
* if the current blob metadata are at their latest version use the {@code metagenerationMatch}
192192
* option: {@code blob.update(newInfo, BlobTargetOption.metagenerationMatch())}.
193+
* <p>
194+
* Original metadata are merged with metadata in the provided {@code blobInfo}. To replace
195+
* metadata instead you first have to unset them. Unsetting metadata can be done by setting the
196+
* provided {@code blobInfo}'s metadata to {@code null}.
197+
* </p>
198+
* <p>
199+
* Example usage of replacing blob's metadata:
200+
* <pre> {@code blob.update(blob.info().toBuilder().metadata(null).build());}
201+
* {@code blob.update(blob.info().toBuilder().metadata(newMetadata).build());}
202+
* </pre>
193203
*
194204
* @param blobInfo new blob's information. Bucket and blob names must match the current ones
195205
* @param options update options
@@ -306,8 +316,7 @@ public Storage storage() {
306316
}
307317

308318
/**
309-
* Gets the requested blobs. If {@code infos.length == 0} an empty list is returned. If
310-
* {@code infos.length > 1} a batch request is used to fetch blobs.
319+
* Gets the requested blobs. A batch request is used to fetch blobs.
311320
*
312321
* @param storage the storage service used to issue the request
313322
* @param blobs the blobs to get
@@ -331,8 +340,12 @@ public Blob apply(BlobInfo f) {
331340
}
332341

333342
/**
334-
* Updates the requested blobs. If {@code infos.length == 0} an empty list is returned. If
335-
* {@code infos.length > 1} a batch request is used to update blobs.
343+
* Updates the requested blobs. A batch request is used to update blobs. Original metadata are
344+
* merged with metadata in the provided {@code BlobInfo} objects. To replace metadata instead
345+
* you first have to unset them. Unsetting metadata can be done by setting the provided
346+
* {@code BlobInfo} objects metadata to {@code null}. See
347+
* {@link #update(com.google.gcloud.storage.BlobInfo,
348+
* com.google.gcloud.storage.Storage.BlobTargetOption...) } for a code example.
336349
*
337350
* @param storage the storage service used to issue the request
338351
* @param infos the blobs to update
@@ -356,8 +369,7 @@ public Blob apply(BlobInfo f) {
356369
}
357370

358371
/**
359-
* Deletes the requested blobs. If {@code infos.length == 0} an empty list is returned. If
360-
* {@code infos.length > 1} a batch request is used to delete blobs.
372+
* Deletes the requested blobs. A batch request is used to delete blobs.
361373
*
362374
* @param storage the storage service used to issue the request
363375
* @param blobs the blobs to delete

gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobInfo.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,19 @@
2727
import com.google.common.base.Function;
2828
import com.google.common.base.MoreObjects;
2929
import com.google.common.collect.ImmutableList;
30-
import com.google.common.collect.ImmutableMap;
30+
import com.google.common.collect.ImmutableSet;
3131
import com.google.common.collect.Lists;
32+
import com.google.common.collect.Maps;
3233

3334
import java.io.Serializable;
3435
import java.math.BigInteger;
36+
import java.util.AbstractMap;
37+
import java.util.Collections;
38+
import java.util.HashMap;
3539
import java.util.List;
3640
import java.util.Map;
3741
import java.util.Objects;
42+
import java.util.Set;
3843

3944
/**
4045
* Google Storage object metadata.
@@ -81,6 +86,17 @@ public StorageObject apply(BlobInfo blobInfo) {
8186
private final String contentLanguage;
8287
private final Integer componentCount;
8388

89+
/**
90+
* This class is meant for internal use only. Users are discouraged from using this class.
91+
*/
92+
public static final class ImmutableEmptyMap<K, V> extends AbstractMap<K, V> {
93+
94+
@Override
95+
public Set<Entry<K, V>> entrySet() {
96+
return ImmutableSet.of();
97+
}
98+
}
99+
84100
public static final class Builder {
85101

86102
private BlobId blobId;
@@ -99,7 +115,7 @@ public static final class Builder {
99115
private String md5;
100116
private String crc32c;
101117
private String mediaLink;
102-
private ImmutableMap<String, String> metadata;
118+
private Map<String, String> metadata;
103119
private Long generation;
104120
private Long metageneration;
105121
private Long deleteTime;
@@ -188,7 +204,8 @@ Builder mediaLink(String mediaLink) {
188204
}
189205

190206
public Builder metadata(Map<String, String> metadata) {
191-
this.metadata = metadata != null ? ImmutableMap.copyOf(metadata) : null;
207+
this.metadata = metadata != null ?
208+
new HashMap(metadata) : Data.<Map>nullOf(ImmutableEmptyMap.class);
192209
return this;
193210
}
194211

@@ -315,7 +332,7 @@ public String mediaLink() {
315332
}
316333

317334
public Map<String, String> metadata() {
318-
return metadata;
335+
return metadata == null || Data.isNull(metadata) ? null : Collections.unmodifiableMap(metadata);
319336
}
320337

321338
public Long generation() {
@@ -402,14 +419,21 @@ public ObjectAccessControl apply(Acl acl) {
402419
if (owner != null) {
403420
storageObject.setOwner(new Owner().setEntity(owner.toPb()));
404421
}
422+
Map<String, String> pbMetadata = metadata;
423+
if (metadata != null && !Data.isNull(metadata)) {
424+
pbMetadata = Maps.newHashMapWithExpectedSize(metadata.size());
425+
for (String key : metadata.keySet()) {
426+
pbMetadata.put(key, firstNonNull(metadata.get(key), Data.<String>nullOf(String.class)));
427+
}
428+
}
429+
storageObject.setMetadata(pbMetadata);
405430
storageObject.setCacheControl(cacheControl);
406431
storageObject.setContentEncoding(contentEncoding);
407432
storageObject.setCrc32c(crc32c);
408433
storageObject.setContentType(contentType);
409434
storageObject.setGeneration(generation);
410435
storageObject.setMd5Hash(md5);
411436
storageObject.setMediaLink(mediaLink);
412-
storageObject.setMetadata(metadata);
413437
storageObject.setMetageneration(metageneration);
414438
storageObject.setContentDisposition(contentDisposition);
415439
storageObject.setComponentCount(componentCount);

gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,15 +685,29 @@ public static Builder builder() {
685685
BucketInfo update(BucketInfo bucketInfo, BucketTargetOption... options);
686686

687687
/**
688-
* Update blob information.
688+
* Update blob information. Original metadata are merged with metadata in the provided
689+
* {@code blobInfo}. To replace metadata instead you first have to unset them. Unsetting metadata
690+
* can be done by setting the provided {@code blobInfo}'s metadata to {@code null}.
691+
* <p>
692+
* Example usage of replacing blob's metadata:
693+
* <pre> {@code service.update(BlobInfo.builder("bucket", "name").metadata(null).build());}
694+
* {@code service.update(BlobInfo.builder("bucket", "name").metadata(newMetadata).build());}
695+
* </pre>
689696
*
690697
* @return the updated blob
691698
* @throws StorageException upon failure
692699
*/
693700
BlobInfo update(BlobInfo blobInfo, BlobTargetOption... options);
694701

695702
/**
696-
* Update blob information.
703+
* Update blob information. Original metadata are merged with metadata in the provided
704+
* {@code blobInfo}. To replace metadata instead you first have to unset them. Unsetting metadata
705+
* can be done by setting the provided {@code blobInfo}'s metadata to {@code null}.
706+
* <p>
707+
* Example usage of replacing blob's metadata:
708+
* <pre> {@code service.update(BlobInfo.builder("bucket", "name").metadata(null).build());}
709+
* {@code service.update(BlobInfo.builder("bucket", "name").metadata(newMetadata).build());}
710+
* </pre>
697711
*
698712
* @return the updated blob
699713
* @throws StorageException upon failure
@@ -828,7 +842,11 @@ public static Builder builder() {
828842
List<BlobInfo> get(BlobId... blobIds);
829843

830844
/**
831-
* Updates the requested blobs. A batch request is used to perform this call.
845+
* Updates the requested blobs. A batch request is used to perform this call. Original metadata
846+
* are merged with metadata in the provided {@code BlobInfo} objects. To replace metadata instead
847+
* you first have to unset them. Unsetting metadata can be done by setting the provided
848+
* {@code BlobInfo} objects metadata to {@code null}. See
849+
* {@link #update(com.google.gcloud.storage.BlobInfo)} for a code example.
832850
*
833851
* @param blobInfos blobs to update
834852
* @return an immutable list of {@code BlobInfo} objects. If a blob does not exist or access to it

gcloud-java-storage/src/test/java/com/google/gcloud/storage/ITStorageTest.java

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static org.junit.Assert.fail;
2626

2727
import com.google.common.collect.ImmutableList;
28+
import com.google.common.collect.ImmutableMap;
2829
import com.google.gcloud.RestorableState;
2930
import com.google.gcloud.storage.testing.RemoteGcsHelper;
3031

@@ -36,8 +37,10 @@
3637
import java.net.URLConnection;
3738
import java.nio.ByteBuffer;
3839
import java.util.Arrays;
40+
import java.util.HashMap;
3941
import java.util.Iterator;
4042
import java.util.List;
43+
import java.util.Map;
4144
import java.util.concurrent.ExecutionException;
4245
import java.util.concurrent.TimeUnit;
4346
import java.util.concurrent.TimeoutException;
@@ -95,8 +98,7 @@ public void testCreateBlob() {
9598
BlobInfo blob = BlobInfo.builder(bucket, blobName).build();
9699
BlobInfo remoteBlob = storage.create(blob, BLOB_BYTE_CONTENT);
97100
assertNotNull(remoteBlob);
98-
assertEquals(blob.bucket(), remoteBlob.bucket());
99-
assertEquals(blob.name(), remoteBlob.name());
101+
assertEquals(blob.blobId(), remoteBlob.blobId());
100102
byte[] readBytes = storage.readAllBytes(bucket, blobName);
101103
assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
102104
assertTrue(storage.delete(bucket, blobName));
@@ -108,8 +110,7 @@ public void testCreateEmptyBlob() {
108110
BlobInfo blob = BlobInfo.builder(bucket, blobName).build();
109111
BlobInfo remoteBlob = storage.create(blob);
110112
assertNotNull(remoteBlob);
111-
assertEquals(blob.bucket(), remoteBlob.bucket());
112-
assertEquals(blob.name(), remoteBlob.name());
113+
assertEquals(blob.blobId(), remoteBlob.blobId());
113114
byte[] readBytes = storage.readAllBytes(bucket, blobName);
114115
assertArrayEquals(new byte[0], readBytes);
115116
assertTrue(storage.delete(bucket, blobName));
@@ -122,8 +123,7 @@ public void testCreateBlobStream() throws UnsupportedEncodingException {
122123
ByteArrayInputStream stream = new ByteArrayInputStream(BLOB_STRING_CONTENT.getBytes(UTF_8));
123124
BlobInfo remoteBlob = storage.create(blob, stream);
124125
assertNotNull(remoteBlob);
125-
assertEquals(blob.bucket(), remoteBlob.bucket());
126-
assertEquals(blob.name(), remoteBlob.name());
126+
assertEquals(blob.blobId(), remoteBlob.blobId());
127127
assertEquals(blob.contentType(), remoteBlob.contentType());
128128
byte[] readBytes = storage.readAllBytes(bucket, blobName);
129129
assertEquals(BLOB_STRING_CONTENT, new String(readBytes, UTF_8));
@@ -168,12 +168,68 @@ public void testUpdateBlob() {
168168
assertNotNull(storage.create(blob));
169169
BlobInfo updatedBlob = storage.update(blob.toBuilder().contentType(CONTENT_TYPE).build());
170170
assertNotNull(updatedBlob);
171-
assertEquals(blob.bucket(), updatedBlob.bucket());
172-
assertEquals(blob.name(), updatedBlob.name());
171+
assertEquals(blob.blobId(), updatedBlob.blobId());
173172
assertEquals(CONTENT_TYPE, updatedBlob.contentType());
174173
assertTrue(storage.delete(bucket, blobName));
175174
}
176175

176+
@Test
177+
public void testUpdateBlobReplaceMetadata() {
178+
String blobName = "test-update-blob-replace-metadata";
179+
ImmutableMap<String, String> metadata = ImmutableMap.of("k1", "a");
180+
ImmutableMap<String, String> newMetadata = ImmutableMap.of("k2", "b");
181+
BlobInfo blob = BlobInfo.builder(bucket, blobName)
182+
.contentType(CONTENT_TYPE)
183+
.metadata(metadata)
184+
.build();
185+
assertNotNull(storage.create(blob));
186+
BlobInfo updatedBlob = storage.update(blob.toBuilder().metadata(null).build());
187+
assertNotNull(updatedBlob);
188+
assertNull(updatedBlob.metadata());
189+
updatedBlob = storage.update(blob.toBuilder().metadata(newMetadata).build());
190+
assertEquals(blob.blobId(), updatedBlob.blobId());
191+
assertEquals(newMetadata, updatedBlob.metadata());
192+
assertTrue(storage.delete(bucket, blobName));
193+
}
194+
195+
@Test
196+
public void testUpdateBlobMergeMetadata() {
197+
String blobName = "test-update-blob-merge-metadata";
198+
ImmutableMap<String, String> metadata = ImmutableMap.of("k1", "a");
199+
ImmutableMap<String, String> newMetadata = ImmutableMap.of("k2", "b");
200+
ImmutableMap<String, String> expectedMetadata = ImmutableMap.of("k1", "a", "k2", "b");
201+
BlobInfo blob = BlobInfo.builder(bucket, blobName)
202+
.contentType(CONTENT_TYPE)
203+
.metadata(metadata)
204+
.build();
205+
assertNotNull(storage.create(blob));
206+
BlobInfo updatedBlob = storage.update(blob.toBuilder().metadata(newMetadata).build());
207+
assertNotNull(updatedBlob);
208+
assertEquals(blob.blobId(), updatedBlob.blobId());
209+
assertEquals(expectedMetadata, updatedBlob.metadata());
210+
assertTrue(storage.delete(bucket, blobName));
211+
}
212+
213+
@Test
214+
public void testUpdateBlobUnsetMetadata() {
215+
String blobName = "test-update-blob-unset-metadata";
216+
ImmutableMap<String, String> metadata = ImmutableMap.of("k1", "a", "k2", "b");
217+
Map<String, String> newMetadata = new HashMap<>();
218+
newMetadata.put("k1", "a");
219+
newMetadata.put("k2", null);
220+
ImmutableMap<String, String> expectedMetadata = ImmutableMap.of("k1", "a");
221+
BlobInfo blob = BlobInfo.builder(bucket, blobName)
222+
.contentType(CONTENT_TYPE)
223+
.metadata(metadata)
224+
.build();
225+
assertNotNull(storage.create(blob));
226+
BlobInfo updatedBlob = storage.update(blob.toBuilder().metadata(newMetadata).build());
227+
assertNotNull(updatedBlob);
228+
assertEquals(blob.blobId(), updatedBlob.blobId());
229+
assertEquals(expectedMetadata, updatedBlob.metadata());
230+
assertTrue(storage.delete(bucket, blobName));
231+
}
232+
177233
@Test
178234
public void testUpdateBlobFail() {
179235
String blobName = "test-update-blob-fail";
@@ -223,8 +279,7 @@ public void testComposeBlob() {
223279
Storage.ComposeRequest.of(ImmutableList.of(sourceBlobName1, sourceBlobName2), targetBlob);
224280
BlobInfo remoteBlob = storage.compose(req);
225281
assertNotNull(remoteBlob);
226-
assertEquals(bucket, remoteBlob.bucket());
227-
assertEquals(targetBlobName, remoteBlob.name());
282+
assertEquals(targetBlob.blobId(), remoteBlob.blobId());
228283
byte[] readBytes = storage.readAllBytes(bucket, targetBlobName);
229284
byte[] composedBytes = Arrays.copyOf(BLOB_BYTE_CONTENT, BLOB_BYTE_CONTENT.length * 2);
230285
System.arraycopy(BLOB_BYTE_CONTENT, 0, composedBytes, BLOB_BYTE_CONTENT.length,
@@ -288,8 +343,7 @@ public void testCopyBlobUpdateMetadata() {
288343
Storage.CopyRequest req = Storage.CopyRequest.of(bucket, sourceBlobName, targetBlob);
289344
BlobInfo remoteBlob = storage.copy(req);
290345
assertNotNull(remoteBlob);
291-
assertEquals(bucket, remoteBlob.bucket());
292-
assertEquals(targetBlobName, remoteBlob.name());
346+
assertEquals(targetBlob.blobId(), remoteBlob.blobId());
293347
assertEquals(CONTENT_TYPE, remoteBlob.contentType());
294348
assertTrue(storage.delete(bucket, sourceBlobName));
295349
assertTrue(storage.delete(bucket, targetBlobName));
@@ -337,10 +391,8 @@ public void testBatchRequest() {
337391
assertEquals(0, updateResponse.gets().size());
338392
BlobInfo remoteUpdatedBlob1 = updateResponse.updates().get(0).get();
339393
BlobInfo remoteUpdatedBlob2 = updateResponse.updates().get(1).get();
340-
assertEquals(bucket, remoteUpdatedBlob1.bucket());
341-
assertEquals(bucket, remoteUpdatedBlob2.bucket());
342-
assertEquals(updatedBlob1.name(), remoteUpdatedBlob1.name());
343-
assertEquals(updatedBlob2.name(), remoteUpdatedBlob2.name());
394+
assertEquals(sourceBlob1.blobId(), remoteUpdatedBlob1.blobId());
395+
assertEquals(sourceBlob2.blobId(), remoteUpdatedBlob2.blobId());
344396
assertEquals(updatedBlob1.contentType(), remoteUpdatedBlob1.contentType());
345397
assertEquals(updatedBlob2.contentType(), remoteUpdatedBlob2.contentType());
346398

@@ -515,8 +567,7 @@ public void testPostSignedUrl() throws IOException {
515567
connection.connect();
516568
BlobInfo remoteBlob = storage.get(bucket, blobName);
517569
assertNotNull(remoteBlob);
518-
assertEquals(bucket, remoteBlob.bucket());
519-
assertEquals(blob.name(), remoteBlob.name());
570+
assertEquals(blob.blobId(), remoteBlob.blobId());
520571
assertTrue(storage.delete(bucket, blobName));
521572
}
522573

@@ -528,11 +579,9 @@ public void testGetBlobs() {
528579
BlobInfo sourceBlob2 = BlobInfo.builder(bucket, sourceBlobName2).build();
529580
assertNotNull(storage.create(sourceBlob1));
530581
assertNotNull(storage.create(sourceBlob2));
531-
List<BlobInfo> remoteInfos = storage.get(sourceBlob1.blobId(), sourceBlob2.blobId());
532-
assertEquals(sourceBlob1.bucket(), remoteInfos.get(0).bucket());
533-
assertEquals(sourceBlob1.name(), remoteInfos.get(0).name());
534-
assertEquals(sourceBlob2.bucket(), remoteInfos.get(1).bucket());
535-
assertEquals(sourceBlob2.name(), remoteInfos.get(1).name());
582+
List<BlobInfo> remoteBlobs = storage.get(sourceBlob1.blobId(), sourceBlob2.blobId());
583+
assertEquals(sourceBlob1.blobId(), remoteBlobs.get(0).blobId());
584+
assertEquals(sourceBlob2.blobId(), remoteBlobs.get(1).blobId());
536585
assertTrue(storage.delete(bucket, sourceBlobName1));
537586
assertTrue(storage.delete(bucket, sourceBlobName2));
538587
}
@@ -545,8 +594,7 @@ public void testGetBlobsFail() {
545594
BlobInfo sourceBlob2 = BlobInfo.builder(bucket, sourceBlobName2).build();
546595
assertNotNull(storage.create(sourceBlob1));
547596
List<BlobInfo> remoteBlobs = storage.get(sourceBlob1.blobId(), sourceBlob2.blobId());
548-
assertEquals(sourceBlob1.bucket(), remoteBlobs.get(0).bucket());
549-
assertEquals(sourceBlob1.name(), remoteBlobs.get(0).name());
597+
assertEquals(sourceBlob1.blobId(), remoteBlobs.get(0).blobId());
550598
assertNull(remoteBlobs.get(1));
551599
assertTrue(storage.delete(bucket, sourceBlobName1));
552600
}
@@ -589,11 +637,9 @@ public void testUpdateBlobs() {
589637
List<BlobInfo> updatedBlobs = storage.update(
590638
remoteBlob1.toBuilder().contentType(CONTENT_TYPE).build(),
591639
remoteBlob2.toBuilder().contentType(CONTENT_TYPE).build());
592-
assertEquals(sourceBlob1.bucket(), updatedBlobs.get(0).bucket());
593-
assertEquals(sourceBlob1.name(), updatedBlobs.get(0).name());
640+
assertEquals(sourceBlob1.blobId(), updatedBlobs.get(0).blobId());
594641
assertEquals(CONTENT_TYPE, updatedBlobs.get(0).contentType());
595-
assertEquals(sourceBlob2.bucket(), updatedBlobs.get(1).bucket());
596-
assertEquals(sourceBlob2.name(), updatedBlobs.get(1).name());
642+
assertEquals(sourceBlob2.blobId(), updatedBlobs.get(1).blobId());
597643
assertEquals(CONTENT_TYPE, updatedBlobs.get(1).contentType());
598644
assertTrue(storage.delete(bucket, sourceBlobName1));
599645
assertTrue(storage.delete(bucket, sourceBlobName2));
@@ -610,8 +656,7 @@ public void testUpdateBlobsFail() {
610656
List<BlobInfo> updatedBlobs = storage.update(
611657
remoteBlob1.toBuilder().contentType(CONTENT_TYPE).build(),
612658
sourceBlob2.toBuilder().contentType(CONTENT_TYPE).build());
613-
assertEquals(sourceBlob1.bucket(), updatedBlobs.get(0).bucket());
614-
assertEquals(sourceBlob1.name(), updatedBlobs.get(0).name());
659+
assertEquals(sourceBlob1.blobId(), updatedBlobs.get(0).blobId());
615660
assertEquals(CONTENT_TYPE, updatedBlobs.get(0).contentType());
616661
assertNull(updatedBlobs.get(1));
617662
assertTrue(storage.delete(bucket, sourceBlobName1));

0 commit comments

Comments
 (0)