diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java index a4a817ead2df..bc75408997f4 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java @@ -190,6 +190,16 @@ public Blob reload(BlobSourceOption... options) { * made on the metadata generation of the current blob. If you want to update the information only * if the current blob metadata are at their latest version use the {@code metagenerationMatch} * option: {@code blob.update(newInfo, BlobTargetOption.metagenerationMatch())}. + *

+ * Original metadata are merged with metadata in the provided {@code blobInfo}. To replace + * metadata instead you first have to unset them. Unsetting metadata can be done by setting the + * provided {@code blobInfo}'s metadata to {@code null}. + *

+ *

+ * Example usage of replacing blob's metadata: + *

    {@code blob.update(blob.info().toBuilder().metadata(null).build());}
+   *    {@code blob.update(blob.info().toBuilder().metadata(newMetadata).build());}
+   * 
* * @param blobInfo new blob's information. Bucket and blob names must match the current ones * @param options update options @@ -306,8 +316,7 @@ public Storage storage() { } /** - * Gets the requested blobs. If {@code infos.length == 0} an empty list is returned. If - * {@code infos.length > 1} a batch request is used to fetch blobs. + * Gets the requested blobs. A batch request is used to fetch blobs. * * @param storage the storage service used to issue the request * @param blobs the blobs to get @@ -331,8 +340,12 @@ public Blob apply(BlobInfo f) { } /** - * Updates the requested blobs. If {@code infos.length == 0} an empty list is returned. If - * {@code infos.length > 1} a batch request is used to update blobs. + * Updates the requested blobs. A batch request is used to update blobs. Original metadata are + * merged with metadata in the provided {@code BlobInfo} objects. To replace metadata instead + * you first have to unset them. Unsetting metadata can be done by setting the provided + * {@code BlobInfo} objects metadata to {@code null}. See + * {@link #update(com.google.gcloud.storage.BlobInfo, + * com.google.gcloud.storage.Storage.BlobTargetOption...) } for a code example. * * @param storage the storage service used to issue the request * @param infos the blobs to update @@ -356,8 +369,7 @@ public Blob apply(BlobInfo f) { } /** - * Deletes the requested blobs. If {@code infos.length == 0} an empty list is returned. If - * {@code infos.length > 1} a batch request is used to delete blobs. + * Deletes the requested blobs. A batch request is used to delete blobs. * * @param storage the storage service used to issue the request * @param blobs the blobs to delete diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobInfo.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobInfo.java index 8e6921bbc20d..ec3ef36708cb 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobInfo.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobInfo.java @@ -27,14 +27,19 @@ import com.google.common.base.Function; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import java.io.Serializable; import java.math.BigInteger; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * Google Storage object metadata. @@ -81,6 +86,17 @@ public StorageObject apply(BlobInfo blobInfo) { private final String contentLanguage; private final Integer componentCount; + /** + * This class is meant for internal use only. Users are discouraged from using this class. + */ + public static final class ImmutableEmptyMap extends AbstractMap { + + @Override + public Set> entrySet() { + return ImmutableSet.of(); + } + } + public static final class Builder { private BlobId blobId; @@ -99,7 +115,7 @@ public static final class Builder { private String md5; private String crc32c; private String mediaLink; - private ImmutableMap metadata; + private Map metadata; private Long generation; private Long metageneration; private Long deleteTime; @@ -188,7 +204,8 @@ Builder mediaLink(String mediaLink) { } public Builder metadata(Map metadata) { - this.metadata = metadata != null ? ImmutableMap.copyOf(metadata) : null; + this.metadata = metadata != null ? + new HashMap(metadata) : Data.nullOf(ImmutableEmptyMap.class); return this; } @@ -315,7 +332,7 @@ public String mediaLink() { } public Map metadata() { - return metadata; + return metadata == null || Data.isNull(metadata) ? null : Collections.unmodifiableMap(metadata); } public Long generation() { @@ -402,6 +419,14 @@ public ObjectAccessControl apply(Acl acl) { if (owner != null) { storageObject.setOwner(new Owner().setEntity(owner.toPb())); } + Map pbMetadata = metadata; + if (metadata != null && !Data.isNull(metadata)) { + pbMetadata = Maps.newHashMapWithExpectedSize(metadata.size()); + for (String key : metadata.keySet()) { + pbMetadata.put(key, firstNonNull(metadata.get(key), Data.nullOf(String.class))); + } + } + storageObject.setMetadata(pbMetadata); storageObject.setCacheControl(cacheControl); storageObject.setContentEncoding(contentEncoding); storageObject.setCrc32c(crc32c); @@ -409,7 +434,6 @@ public ObjectAccessControl apply(Acl acl) { storageObject.setGeneration(generation); storageObject.setMd5Hash(md5); storageObject.setMediaLink(mediaLink); - storageObject.setMetadata(metadata); storageObject.setMetageneration(metageneration); storageObject.setContentDisposition(contentDisposition); storageObject.setComponentCount(componentCount); diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java index f9a1c00d4bec..267bcdf5b24c 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java @@ -683,7 +683,14 @@ public static Builder builder() { BucketInfo update(BucketInfo bucketInfo, BucketTargetOption... options); /** - * Update blob information. + * Update blob information. Original metadata are merged with metadata in the provided + * {@code blobInfo}. To replace metadata instead you first have to unset them. Unsetting metadata + * can be done by setting the provided {@code blobInfo}'s metadata to {@code null}. + *

+ * Example usage of replacing blob's metadata: + *

    {@code service.update(BlobInfo.builder("bucket", "name").metadata(null).build());}
+   *    {@code service.update(BlobInfo.builder("bucket", "name").metadata(newMetadata).build());}
+   * 
* * @return the updated blob * @throws StorageException upon failure @@ -691,7 +698,14 @@ public static Builder builder() { BlobInfo update(BlobInfo blobInfo, BlobTargetOption... options); /** - * Update blob information. + * Update blob information. Original metadata are merged with metadata in the provided + * {@code blobInfo}. To replace metadata instead you first have to unset them. Unsetting metadata + * can be done by setting the provided {@code blobInfo}'s metadata to {@code null}. + *

+ * Example usage of replacing blob's metadata: + *

    {@code service.update(BlobInfo.builder("bucket", "name").metadata(null).build());}
+   *    {@code service.update(BlobInfo.builder("bucket", "name").metadata(newMetadata).build());}
+   * 
* * @return the updated blob * @throws StorageException upon failure @@ -826,7 +840,11 @@ public static Builder builder() { List get(BlobId... blobIds); /** - * Updates the requested blobs. A batch request is used to perform this call. + * Updates the requested blobs. A batch request is used to perform this call. Original metadata + * are merged with metadata in the provided {@code BlobInfo} objects. To replace metadata instead + * you first have to unset them. Unsetting metadata can be done by setting the provided + * {@code BlobInfo} objects metadata to {@code null}. See + * {@link #update(com.google.gcloud.storage.BlobInfo)} for a code example. * * @param blobInfos blobs to update * @return an immutable list of {@code BlobInfo} objects. If a blob does not exist or access to it diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ITStorageTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ITStorageTest.java index 2747444d1f27..3acb09a18080 100644 --- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ITStorageTest.java +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ITStorageTest.java @@ -25,6 +25,7 @@ import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.gcloud.RestorableState; import com.google.gcloud.storage.testing.RemoteGcsHelper; @@ -36,8 +37,10 @@ import java.net.URLConnection; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -95,8 +98,7 @@ public void testCreateBlob() { BlobInfo blob = BlobInfo.builder(bucket, blobName).build(); BlobInfo remoteBlob = storage.create(blob, BLOB_BYTE_CONTENT); assertNotNull(remoteBlob); - assertEquals(blob.bucket(), remoteBlob.bucket()); - assertEquals(blob.name(), remoteBlob.name()); + assertEquals(blob.blobId(), remoteBlob.blobId()); byte[] readBytes = storage.readAllBytes(bucket, blobName); assertArrayEquals(BLOB_BYTE_CONTENT, readBytes); assertTrue(storage.delete(bucket, blobName)); @@ -108,8 +110,7 @@ public void testCreateEmptyBlob() { BlobInfo blob = BlobInfo.builder(bucket, blobName).build(); BlobInfo remoteBlob = storage.create(blob); assertNotNull(remoteBlob); - assertEquals(blob.bucket(), remoteBlob.bucket()); - assertEquals(blob.name(), remoteBlob.name()); + assertEquals(blob.blobId(), remoteBlob.blobId()); byte[] readBytes = storage.readAllBytes(bucket, blobName); assertArrayEquals(new byte[0], readBytes); assertTrue(storage.delete(bucket, blobName)); @@ -122,8 +123,7 @@ public void testCreateBlobStream() throws UnsupportedEncodingException { ByteArrayInputStream stream = new ByteArrayInputStream(BLOB_STRING_CONTENT.getBytes(UTF_8)); BlobInfo remoteBlob = storage.create(blob, stream); assertNotNull(remoteBlob); - assertEquals(blob.bucket(), remoteBlob.bucket()); - assertEquals(blob.name(), remoteBlob.name()); + assertEquals(blob.blobId(), remoteBlob.blobId()); assertEquals(blob.contentType(), remoteBlob.contentType()); byte[] readBytes = storage.readAllBytes(bucket, blobName); assertEquals(BLOB_STRING_CONTENT, new String(readBytes, UTF_8)); @@ -168,12 +168,68 @@ public void testUpdateBlob() { assertNotNull(storage.create(blob)); BlobInfo updatedBlob = storage.update(blob.toBuilder().contentType(CONTENT_TYPE).build()); assertNotNull(updatedBlob); - assertEquals(blob.bucket(), updatedBlob.bucket()); - assertEquals(blob.name(), updatedBlob.name()); + assertEquals(blob.blobId(), updatedBlob.blobId()); assertEquals(CONTENT_TYPE, updatedBlob.contentType()); assertTrue(storage.delete(bucket, blobName)); } + @Test + public void testUpdateBlobReplaceMetadata() { + String blobName = "test-update-blob-replace-metadata"; + ImmutableMap metadata = ImmutableMap.of("k1", "a"); + ImmutableMap newMetadata = ImmutableMap.of("k2", "b"); + BlobInfo blob = BlobInfo.builder(bucket, blobName) + .contentType(CONTENT_TYPE) + .metadata(metadata) + .build(); + assertNotNull(storage.create(blob)); + BlobInfo updatedBlob = storage.update(blob.toBuilder().metadata(null).build()); + assertNotNull(updatedBlob); + assertNull(updatedBlob.metadata()); + updatedBlob = storage.update(blob.toBuilder().metadata(newMetadata).build()); + assertEquals(blob.blobId(), updatedBlob.blobId()); + assertEquals(newMetadata, updatedBlob.metadata()); + assertTrue(storage.delete(bucket, blobName)); + } + + @Test + public void testUpdateBlobMergeMetadata() { + String blobName = "test-update-blob-merge-metadata"; + ImmutableMap metadata = ImmutableMap.of("k1", "a"); + ImmutableMap newMetadata = ImmutableMap.of("k2", "b"); + ImmutableMap expectedMetadata = ImmutableMap.of("k1", "a", "k2", "b"); + BlobInfo blob = BlobInfo.builder(bucket, blobName) + .contentType(CONTENT_TYPE) + .metadata(metadata) + .build(); + assertNotNull(storage.create(blob)); + BlobInfo updatedBlob = storage.update(blob.toBuilder().metadata(newMetadata).build()); + assertNotNull(updatedBlob); + assertEquals(blob.blobId(), updatedBlob.blobId()); + assertEquals(expectedMetadata, updatedBlob.metadata()); + assertTrue(storage.delete(bucket, blobName)); + } + + @Test + public void testUpdateBlobUnsetMetadata() { + String blobName = "test-update-blob-unset-metadata"; + ImmutableMap metadata = ImmutableMap.of("k1", "a", "k2", "b"); + Map newMetadata = new HashMap<>(); + newMetadata.put("k1", "a"); + newMetadata.put("k2", null); + ImmutableMap expectedMetadata = ImmutableMap.of("k1", "a"); + BlobInfo blob = BlobInfo.builder(bucket, blobName) + .contentType(CONTENT_TYPE) + .metadata(metadata) + .build(); + assertNotNull(storage.create(blob)); + BlobInfo updatedBlob = storage.update(blob.toBuilder().metadata(newMetadata).build()); + assertNotNull(updatedBlob); + assertEquals(blob.blobId(), updatedBlob.blobId()); + assertEquals(expectedMetadata, updatedBlob.metadata()); + assertTrue(storage.delete(bucket, blobName)); + } + @Test public void testUpdateBlobFail() { String blobName = "test-update-blob-fail"; @@ -223,8 +279,7 @@ public void testComposeBlob() { Storage.ComposeRequest.of(ImmutableList.of(sourceBlobName1, sourceBlobName2), targetBlob); BlobInfo remoteBlob = storage.compose(req); assertNotNull(remoteBlob); - assertEquals(bucket, remoteBlob.bucket()); - assertEquals(targetBlobName, remoteBlob.name()); + assertEquals(targetBlob.blobId(), remoteBlob.blobId()); byte[] readBytes = storage.readAllBytes(bucket, targetBlobName); byte[] composedBytes = Arrays.copyOf(BLOB_BYTE_CONTENT, BLOB_BYTE_CONTENT.length * 2); System.arraycopy(BLOB_BYTE_CONTENT, 0, composedBytes, BLOB_BYTE_CONTENT.length, @@ -288,8 +343,7 @@ public void testCopyBlobUpdateMetadata() { Storage.CopyRequest req = Storage.CopyRequest.of(bucket, sourceBlobName, targetBlob); BlobInfo remoteBlob = storage.copy(req); assertNotNull(remoteBlob); - assertEquals(bucket, remoteBlob.bucket()); - assertEquals(targetBlobName, remoteBlob.name()); + assertEquals(targetBlob.blobId(), remoteBlob.blobId()); assertEquals(CONTENT_TYPE, remoteBlob.contentType()); assertTrue(storage.delete(bucket, sourceBlobName)); assertTrue(storage.delete(bucket, targetBlobName)); @@ -337,10 +391,8 @@ public void testBatchRequest() { assertEquals(0, updateResponse.gets().size()); BlobInfo remoteUpdatedBlob1 = updateResponse.updates().get(0).get(); BlobInfo remoteUpdatedBlob2 = updateResponse.updates().get(1).get(); - assertEquals(bucket, remoteUpdatedBlob1.bucket()); - assertEquals(bucket, remoteUpdatedBlob2.bucket()); - assertEquals(updatedBlob1.name(), remoteUpdatedBlob1.name()); - assertEquals(updatedBlob2.name(), remoteUpdatedBlob2.name()); + assertEquals(sourceBlob1.blobId(), remoteUpdatedBlob1.blobId()); + assertEquals(sourceBlob2.blobId(), remoteUpdatedBlob2.blobId()); assertEquals(updatedBlob1.contentType(), remoteUpdatedBlob1.contentType()); assertEquals(updatedBlob2.contentType(), remoteUpdatedBlob2.contentType()); @@ -515,8 +567,7 @@ public void testPostSignedUrl() throws IOException { connection.connect(); BlobInfo remoteBlob = storage.get(bucket, blobName); assertNotNull(remoteBlob); - assertEquals(bucket, remoteBlob.bucket()); - assertEquals(blob.name(), remoteBlob.name()); + assertEquals(blob.blobId(), remoteBlob.blobId()); assertTrue(storage.delete(bucket, blobName)); } @@ -528,11 +579,9 @@ public void testGetBlobs() { BlobInfo sourceBlob2 = BlobInfo.builder(bucket, sourceBlobName2).build(); assertNotNull(storage.create(sourceBlob1)); assertNotNull(storage.create(sourceBlob2)); - List remoteInfos = storage.get(sourceBlob1.blobId(), sourceBlob2.blobId()); - assertEquals(sourceBlob1.bucket(), remoteInfos.get(0).bucket()); - assertEquals(sourceBlob1.name(), remoteInfos.get(0).name()); - assertEquals(sourceBlob2.bucket(), remoteInfos.get(1).bucket()); - assertEquals(sourceBlob2.name(), remoteInfos.get(1).name()); + List remoteBlobs = storage.get(sourceBlob1.blobId(), sourceBlob2.blobId()); + assertEquals(sourceBlob1.blobId(), remoteBlobs.get(0).blobId()); + assertEquals(sourceBlob2.blobId(), remoteBlobs.get(1).blobId()); assertTrue(storage.delete(bucket, sourceBlobName1)); assertTrue(storage.delete(bucket, sourceBlobName2)); } @@ -545,8 +594,7 @@ public void testGetBlobsFail() { BlobInfo sourceBlob2 = BlobInfo.builder(bucket, sourceBlobName2).build(); assertNotNull(storage.create(sourceBlob1)); List remoteBlobs = storage.get(sourceBlob1.blobId(), sourceBlob2.blobId()); - assertEquals(sourceBlob1.bucket(), remoteBlobs.get(0).bucket()); - assertEquals(sourceBlob1.name(), remoteBlobs.get(0).name()); + assertEquals(sourceBlob1.blobId(), remoteBlobs.get(0).blobId()); assertNull(remoteBlobs.get(1)); assertTrue(storage.delete(bucket, sourceBlobName1)); } @@ -589,11 +637,9 @@ public void testUpdateBlobs() { List updatedBlobs = storage.update( remoteBlob1.toBuilder().contentType(CONTENT_TYPE).build(), remoteBlob2.toBuilder().contentType(CONTENT_TYPE).build()); - assertEquals(sourceBlob1.bucket(), updatedBlobs.get(0).bucket()); - assertEquals(sourceBlob1.name(), updatedBlobs.get(0).name()); + assertEquals(sourceBlob1.blobId(), updatedBlobs.get(0).blobId()); assertEquals(CONTENT_TYPE, updatedBlobs.get(0).contentType()); - assertEquals(sourceBlob2.bucket(), updatedBlobs.get(1).bucket()); - assertEquals(sourceBlob2.name(), updatedBlobs.get(1).name()); + assertEquals(sourceBlob2.blobId(), updatedBlobs.get(1).blobId()); assertEquals(CONTENT_TYPE, updatedBlobs.get(1).contentType()); assertTrue(storage.delete(bucket, sourceBlobName1)); assertTrue(storage.delete(bucket, sourceBlobName2)); @@ -610,8 +656,7 @@ public void testUpdateBlobsFail() { List updatedBlobs = storage.update( remoteBlob1.toBuilder().contentType(CONTENT_TYPE).build(), sourceBlob2.toBuilder().contentType(CONTENT_TYPE).build()); - assertEquals(sourceBlob1.bucket(), updatedBlobs.get(0).bucket()); - assertEquals(sourceBlob1.name(), updatedBlobs.get(0).name()); + assertEquals(sourceBlob1.blobId(), updatedBlobs.get(0).blobId()); assertEquals(CONTENT_TYPE, updatedBlobs.get(0).contentType()); assertNull(updatedBlobs.get(1)); assertTrue(storage.delete(bucket, sourceBlobName1));