From 9ebf21ef565cac38ec7ca3eec8acfc0794e17f3c Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Wed, 21 Oct 2015 15:20:20 +0200 Subject: [PATCH 1/4] Make storage.update use normal update, add storage.patch - Add update method to StoragRpc - Make Storage.update use StorageRpc.update instead of patch - Add Storage.patch(blobInfo) method - Add support for patches to BatchRequest (both Storage and StorageRpc) - Add static Storage.patch(BlobInfo...) method for easily batching patches - Update and add new tests - Update and add javadoc for Storage.update and Storage.patch --- .../google/gcloud/spi/DefaultStorageRpc.java | 42 ++++- .../com/google/gcloud/spi/StorageRpc.java | 19 +- .../google/gcloud/storage/BatchRequest.java | 20 ++- .../google/gcloud/storage/BatchResponse.java | 16 +- .../com/google/gcloud/storage/Storage.java | 54 +++++- .../google/gcloud/storage/StorageImpl.java | 47 ++++- .../gcloud/storage/BatchRequestTest.java | 14 ++ .../gcloud/storage/BatchResponseTest.java | 9 +- .../com/google/gcloud/storage/BucketTest.java | 4 +- .../google/gcloud/storage/ITStorageTest.java | 170 ++++++++++++++---- .../gcloud/storage/SerializationTest.java | 3 +- .../gcloud/storage/StorageImplTest.java | 111 +++++++++++- 12 files changed, 440 insertions(+), 69 deletions(-) diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java index 52820171cf29..5b89d170fedf 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java @@ -225,6 +225,27 @@ public Bucket patch(Bucket bucket, Map options) { } } + @Override + public StorageObject update(StorageObject storageObject, Map options) { + try { + return updateRequest(storageObject, options).execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + private Storage.Objects.Update updateRequest(StorageObject storageObject, Map options) + throws IOException { + return storage.objects() + .update(storageObject.getBucket(), storageObject.getName(), storageObject) + .setProjection(DEFAULT_PROJECTION) + .setPredefinedAcl(PREDEFINED_ACL.getString(options)) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(options)) + .setIfGenerationNotMatch(IF_GENERATION_NOT_MATCH.getLong(options)); + } + @Override public StorageObject patch(StorageObject storageObject, Map options) { try { @@ -374,6 +395,8 @@ public BatchResponse batch(BatchRequest request) throws StorageException { Maps.newConcurrentMap(); final Map> gets = Maps.newConcurrentMap(); + final Map> patches = + Maps.newConcurrentMap(); try { for (final Tuple> tuple : request.toDelete) { deleteRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { @@ -389,7 +412,7 @@ public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { }); } for (final Tuple> tuple : request.toUpdate) { - patchRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { + updateRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { @Override public void onSuccess(StorageObject storageObject, HttpHeaders responseHeaders) { updates.put(tuple.x(), @@ -418,11 +441,26 @@ public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { } }); } + for (final Tuple> tuple : request.toPatch) { + patchRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { + @Override + public void onSuccess(StorageObject storageObject, HttpHeaders responseHeaders) { + patches.put(tuple.x(), + Tuple.of(storageObject, null)); + } + + @Override + public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { + patches.put(tuple.x(), + Tuple.of(null, translate(e))); + } + }); + } batch.execute(); } catch (IOException ex) { throw translate(ex); } - return new BatchResponse(deletes, updates, gets); + return new BatchResponse(deletes, updates, gets, patches); } @Override diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java index b7ac99bf909e..ba2408f881a3 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java @@ -104,16 +104,20 @@ class BatchRequest { public final List>> toDelete; public final List>> toUpdate; public final List>> toGet; + public final List>> toPatch; public BatchRequest(Iterable>> toDelete, Iterable>> toUpdate, - Iterable>> toGet) { + Iterable>> toGet, + Iterable>> toPatch) { this.toDelete = ImmutableList.copyOf( firstNonNull(toDelete, ImmutableList.>>of())); this.toUpdate = ImmutableList.copyOf( firstNonNull(toUpdate, ImmutableList.>>of())); this.toGet = ImmutableList.copyOf( firstNonNull(toGet, ImmutableList.>>of())); + this.toPatch = ImmutableList.copyOf( + firstNonNull(toPatch, ImmutableList.>>of())); } } @@ -122,13 +126,16 @@ class BatchResponse { public final Map> deletes; public final Map> updates; public final Map> gets; + public final Map> patches; public BatchResponse(Map> deletes, Map> updates, - Map> gets) { + Map> gets, + Map> patches) { this.deletes = ImmutableMap.copyOf(deletes); this.updates = ImmutableMap.copyOf(updates); this.gets = ImmutableMap.copyOf(gets); + this.patches = ImmutableMap.copyOf(patches); } } @@ -144,13 +151,13 @@ Tuple> list(String bucket, Map option Bucket get(Bucket bucket, Map options) throws StorageException; - StorageObject get(StorageObject object, Map options) - throws StorageException; + StorageObject get(StorageObject object, Map options) throws StorageException; Bucket patch(Bucket bucket, Map options) throws StorageException; - StorageObject patch(StorageObject storageObject, Map options) - throws StorageException; + StorageObject update(StorageObject storageObject, Map options) throws StorageException; + + StorageObject patch(StorageObject storageObject, Map options) throws StorageException; boolean delete(Bucket bucket, Map options) throws StorageException; diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchRequest.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchRequest.java index 6e815648497a..6573232e6238 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchRequest.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchRequest.java @@ -36,12 +36,14 @@ public final class BatchRequest implements Serializable { private final Map> toDelete; private final Map> toUpdate; private final Map> toGet; + private final Map> toPatch; public static class Builder { private Map> toDelete = new LinkedHashMap<>(); private Map> toUpdate = new LinkedHashMap<>(); private Map> toGet = new LinkedHashMap<>(); + private Map> toPatch = new LinkedHashMap<>(); private Builder() {} @@ -85,6 +87,14 @@ public Builder get(BlobId blob, BlobSourceOption... options) { return this; } + /** + * Patch the given blob. + */ + public Builder patch(BlobInfo blobInfo, BlobTargetOption... options) { + toPatch.put(blobInfo, Lists.newArrayList(options)); + return this; + } + public BatchRequest build() { return new BatchRequest(this); } @@ -94,11 +104,12 @@ private BatchRequest(Builder builder) { toDelete = ImmutableMap.copyOf(builder.toDelete); toUpdate = ImmutableMap.copyOf(builder.toUpdate); toGet = ImmutableMap.copyOf(builder.toGet); + toPatch = ImmutableMap.copyOf(builder.toPatch); } @Override public int hashCode() { - return Objects.hash(toDelete, toUpdate, toGet); + return Objects.hash(toDelete, toUpdate, toGet, toPatch); } @Override @@ -109,7 +120,8 @@ public boolean equals(Object obj) { BatchRequest other = (BatchRequest) obj; return Objects.equals(toDelete, other.toDelete) && Objects.equals(toUpdate, other.toUpdate) - && Objects.equals(toGet, other.toGet); + && Objects.equals(toGet, other.toGet) + && Objects.equals(toPatch, other.toPatch); } public Map> toDelete() { @@ -124,6 +136,10 @@ public Map> toGet() { return toGet; } + public Map> toPatch() { + return toPatch; + } + public static Builder builder() { return new Builder(); } diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java index 02b1ca966622..7a5dc4464b76 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java @@ -33,6 +33,7 @@ public final class BatchResponse implements Serializable { private final List> deleteResult; private final List> updateResult; private final List> getResult; + private final List> patchResult; public static class Result implements Serializable { @@ -114,15 +115,16 @@ static Result empty() { } public BatchResponse(List> deleteResult, List> updateResult, - List> getResult) { + List> getResult, List> patchResult) { this.deleteResult = ImmutableList.copyOf(deleteResult); this.updateResult = ImmutableList.copyOf(updateResult); this.getResult = ImmutableList.copyOf(getResult); + this.patchResult = ImmutableList.copyOf(patchResult); } @Override public int hashCode() { - return Objects.hash(deleteResult, updateResult, getResult); + return Objects.hash(deleteResult, updateResult, getResult, patchResult); } @Override @@ -133,7 +135,8 @@ public boolean equals(Object obj) { BatchResponse other = (BatchResponse) obj; return Objects.equals(deleteResult, other.deleteResult) && Objects.equals(updateResult, other.updateResult) - && Objects.equals(updateResult, other.updateResult); + && Objects.equals(updateResult, other.updateResult) + && Objects.equals(patchResult, other.patchResult); } /** @@ -156,4 +159,11 @@ public List> updates() { public List> gets() { return getResult; } + + /** + * Returns the results for the patch operations using the request order. + */ + public List> patches() { + return patchResult; + } } 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..af9982478cc9 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,8 @@ public static Builder builder() { BucketInfo update(BucketInfo bucketInfo, BucketTargetOption... options); /** - * Update blob information. + * Update blob information. All the blob's metadata will be replaced using values from the given + * {@code blobInfo}. Update will fail if an incomplete {@code blobInfo} is provided. * * @return the updated blob * @throws StorageException upon failure @@ -698,6 +699,30 @@ public static Builder builder() { */ BlobInfo update(BlobInfo blobInfo); + /** + * Update blob information according to the patch semantics. Original metadata are merged with + * metadata in the provided {@code blobInfo}. To replace metadata use + * {@link #update(com.google.gcloud.storage.BlobInfo)} instead. + * + * @return the patched blob + * @throws StorageException upon failure + * @see + * Patch (partial update) + */ + BlobInfo patch(BlobInfo blobInfo, BlobTargetOption... options); + + /** + * Update blob information according to the patch semantics. Original metadata are merged with + * metadata in the provided {@code blobInfo}. To replace metadata use + * {@link #update(com.google.gcloud.storage.BlobInfo)} instead. + * + * @return the patched blob + * @throws StorageException upon failure + * @see + * Patch (partial update) + */ + BlobInfo patch(BlobInfo blobInfo); + /** * Delete the requested bucket. * @@ -819,18 +844,20 @@ public static Builder builder() { * Gets the requested blobs. A batch request is used to perform this call. * * @param blobIds blobs to get - * @return an immutable list of {@code BlobInfo} objects. If a blob does not exist or access to it - * has been denied the corresponding item in the list is {@code null}. + * @return an immutable list of {@code BlobInfo} objects. If the blob does not exist or access to + * it has been denied the corresponding item in the list is {@code null}. * @throws StorageException upon failure */ 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. Blobs metadata will + * be replaced using values from the given {@code BlobInfo} objects. * * @param blobInfos blobs to update - * @return an immutable list of {@code BlobInfo} objects. If a blob does not exist or access to it - * has been denied the corresponding item in the list is {@code null}. + * @return an immutable list of {@code BlobInfo} objects. If an incomplete {@code blobInfo} is + * provided, the blob does not exist or access to it has been denied the corresponding item in + * the list is {@code null}. * @throws StorageException upon failure */ List update(BlobInfo... blobInfos); @@ -845,4 +872,19 @@ public static Builder builder() { * @throws StorageException upon failure */ List delete(BlobId... blobIds); + + /** + * Updates the requested blobs according to the patch semantics. A batch request is used to + * perform this call. Original metadata are merged with metadata in the provided {@code BlobInfo} + * objects. To replace metadata use {@link #update(com.google.gcloud.storage.BlobInfo...)} + * instead. + * + * @param blobInfos blobs to patch + * @return an immutable list of {@code BlobInfo} objects. If a blob does not exist or access to it + * has been denied the corresponding item in the list is {@code null}. + * @throws StorageException upon failure + * @see + * Patch (partial update) + */ + List patch(BlobInfo... blobInfos); } diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java index 6edd2713fded..b04cc0825f6d 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java @@ -367,7 +367,7 @@ public BlobInfo update(BlobInfo blobInfo, BlobTargetOption... options) { return BlobInfo.fromPb(runWithRetries(new Callable() { @Override public StorageObject call() { - return storageRpc.patch(storageObject, optionsMap); + return storageRpc.update(storageObject, optionsMap); } }, options().retryParams(), EXCEPTION_HANDLER)); } catch (RetryHelperException e) { @@ -380,6 +380,27 @@ public BlobInfo update(BlobInfo blobInfo) { return update(blobInfo, new BlobTargetOption[0]); } + @Override + public BlobInfo patch(BlobInfo blobInfo, BlobTargetOption... options) { + final StorageObject storageObject = blobInfo.toPb(); + final Map optionsMap = optionMap(blobInfo, options); + try { + return BlobInfo.fromPb(runWithRetries(new Callable() { + @Override + public StorageObject call() { + return storageRpc.patch(storageObject, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER)); + } catch (RetryHelperException e) { + throw StorageException.translateAndThrow(e); + } + } + + @Override + public BlobInfo patch(BlobInfo blobInfo) { + return patch(blobInfo, new BlobTargetOption[0]); + } + @Override public boolean delete(String bucket, BucketSourceOption... options) { final com.google.api.services.storage.model.Bucket bucketPb = BucketInfo.of(bucket).toPb(); @@ -512,15 +533,25 @@ public BatchResponse apply(BatchRequest batchRequest) { Map optionsMap = optionMap(null, null, entry.getValue()); toGet.add(Tuple.>of(blob.toPb(), optionsMap)); } + List>> toPatch = + Lists.newArrayListWithCapacity(batchRequest.toPatch().size()); + for (Map.Entry> entry : batchRequest.toPatch().entrySet()) { + BlobInfo blobInfo = entry.getKey(); + Map optionsMap = + optionMap(blobInfo.generation(), blobInfo.metageneration(), entry.getValue()); + toPatch.add(Tuple.>of(blobInfo.toPb(), optionsMap)); + } StorageRpc.BatchResponse response = - storageRpc.batch(new StorageRpc.BatchRequest(toDelete, toUpdate, toGet)); + storageRpc.batch(new StorageRpc.BatchRequest(toDelete, toUpdate, toGet, toPatch)); List> deletes = transformBatchResult( toDelete, response.deletes, Functions.identity()); List> updates = transformBatchResult( toUpdate, response.updates, BlobInfo.FROM_PB_FUNCTION); List> gets = transformBatchResult( toGet, response.gets, BlobInfo.FROM_PB_FUNCTION, HTTP_NOT_FOUND); - return new BatchResponse(deletes, updates, gets); + List> patches = transformBatchResult( + toPatch, response.patches, BlobInfo.FROM_PB_FUNCTION); + return new BatchResponse(deletes, updates, gets, patches); } private List> transformBatchResult( @@ -664,6 +695,16 @@ public List delete(BlobId... blobIds) { return Collections.unmodifiableList(transformResultList(response.deletes(), Boolean.FALSE)); } + @Override + public List patch(BlobInfo... blobInfos) { + BatchRequest.Builder requestBuilder = BatchRequest.builder(); + for (BlobInfo blobInfo : blobInfos) { + requestBuilder.patch(blobInfo); + } + BatchResponse response = apply(requestBuilder.build()); + return Collections.unmodifiableList(transformResultList(response.patches(), null)); + } + private static List transformResultList( List> results, final T errorValue) { return Lists.transform(results, new Function, T>() { diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchRequestTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchRequestTest.java index 96b73c871468..07ba73e870c2 100644 --- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchRequestTest.java +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchRequestTest.java @@ -40,6 +40,8 @@ public void testBatchRequest() { BlobSourceOption.metagenerationMatch(2)) .update(BlobInfo.builder("b2", "o1").build(), BlobTargetOption.predefinedAcl(PUBLIC_READ)) .update(BlobInfo.builder("b2", "o2").build()) + .patch(BlobInfo.builder("b4", "o1").build(), BlobTargetOption.metagenerationMatch()) + .patch(BlobInfo.builder("b4", "o2").build()) .get("b3", "o1") .get("b3", "o2", BlobSourceOption.generationMatch(1)) .get("b3", "o3") @@ -81,5 +83,17 @@ public void testBatchRequest() { assertEquals(BlobId.of("b3", "o3"), get.getKey()); assertTrue(Iterables.isEmpty(get.getValue())); assertFalse(gets.hasNext()); + + Iterator>> patches = request + .toPatch().entrySet().iterator(); + Entry> patch = patches.next(); + assertEquals(BlobInfo.builder("b4", "o1").build(), patch.getKey()); + assertEquals(1, Iterables.size(patch.getValue())); + assertEquals(BlobTargetOption.metagenerationMatch(), + Iterables.getFirst(patch.getValue(), null)); + patch = patches.next(); + assertEquals(BlobInfo.builder("b4", "o2").build(), patch.getKey()); + assertTrue(Iterables.isEmpty(patch.getValue())); + assertFalse(patches.hasNext()); } } diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchResponseTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchResponseTest.java index 59c1da91b3fd..f85656b94e5f 100644 --- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchResponseTest.java +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchResponseTest.java @@ -30,16 +30,21 @@ public class BatchResponseTest { private static final BlobInfo BLOB_INFO_1 = BlobInfo.builder("b", "o1").build(); private static final BlobInfo BLOB_INFO_2 = BlobInfo.builder("b", "o2").build(); private static final BlobInfo BLOB_INFO_3 = BlobInfo.builder("b", "o3").build(); + private static final BlobInfo BLOB_INFO_4 = BlobInfo.builder("b", "o4").build(); @Test public void testBatchResponse() { List> deletes = ImmutableList.of(Result.of(true), Result.of(false)); - List> updates = ImmutableList.of(Result.of(BLOB_INFO_1), Result.of(BLOB_INFO_2)); + List> updates = + ImmutableList.of(Result.of(BLOB_INFO_1), Result.of(BLOB_INFO_2)); List> gets = ImmutableList.of(Result.of(BLOB_INFO_2), Result.of(BLOB_INFO_3)); - BatchResponse response = new BatchResponse(deletes, updates, gets); + List> patches = + ImmutableList.of(Result.of(BLOB_INFO_3), Result.of(BLOB_INFO_4)); + BatchResponse response = new BatchResponse(deletes, updates, gets, patches); assertEquals(deletes, response.deletes()); assertEquals(updates, response.updates()); assertEquals(gets, response.gets()); + assertEquals(patches, response.patches()); } } diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketTest.java index af156cb932ee..8697d85424f0 100644 --- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketTest.java +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketTest.java @@ -140,8 +140,8 @@ public void testGetAll() throws Exception { for (BlobInfo info : BLOB_INFO_RESULTS) { batchResultList.add(new Result<>(info)); } - BatchResponse response = - new BatchResponse(Collections.EMPTY_LIST, Collections.EMPTY_LIST, batchResultList); + BatchResponse response = new BatchResponse( + Collections.EMPTY_LIST, Collections.EMPTY_LIST, batchResultList, Collections.EMPTY_LIST); expect(storage.apply(capture(capturedBatchRequest))).andReturn(response); replay(storage); List blobs = bucket.get("n1", "n2", "n3"); 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..9f510ab31a35 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; @@ -164,13 +165,15 @@ public void testCreateBlobMd5Fail() throws UnsupportedEncodingException { @Test public void testUpdateBlob() { String blobName = "test-update-blob"; - BlobInfo blob = BlobInfo.builder(bucket, blobName).build(); - assertNotNull(storage.create(blob)); - BlobInfo updatedBlob = storage.update(blob.toBuilder().contentType(CONTENT_TYPE).build()); + BlobInfo blob = BlobInfo.builder(bucket, blobName).metadata(ImmutableMap.of("k1", "v")).build(); + BlobInfo remoteBlob = storage.create(blob); + assertNotNull(remoteBlob); + BlobInfo toUpdateBlob = remoteBlob.toBuilder().metadata(ImmutableMap.of("k2", "v")).build(); + BlobInfo updatedBlob = storage.update(toUpdateBlob); assertNotNull(updatedBlob); - assertEquals(blob.bucket(), updatedBlob.bucket()); - assertEquals(blob.name(), updatedBlob.name()); - assertEquals(CONTENT_TYPE, updatedBlob.contentType()); + assertEquals(toUpdateBlob.bucket(), updatedBlob.bucket()); + assertEquals(toUpdateBlob.name(), updatedBlob.name()); + assertEquals(toUpdateBlob.metadata(), updatedBlob.metadata()); assertTrue(storage.delete(bucket, blobName)); } @@ -180,7 +183,38 @@ public void testUpdateBlobFail() { BlobInfo blob = BlobInfo.builder(bucket, blobName).build(); assertNotNull(storage.create(blob)); try { - storage.update(blob.toBuilder().contentType(CONTENT_TYPE).generation(-1L).build(), + storage.update(blob.toBuilder().build()); + fail("StorageException was expected"); + } catch (StorageException ex) { + // expected + } + assertTrue(storage.delete(bucket, blobName)); + } + + @Test + public void testPatchBlob() { + String blobName = "test-patch-blob"; + BlobInfo blob = BlobInfo.builder(bucket, blobName).metadata(ImmutableMap.of("k1", "v")).build(); + assertNotNull(storage.create(blob)); + BlobInfo toUpdateBlob = BlobInfo.builder(bucket, blobName) + .contentType(CONTENT_TYPE) + .metadata(ImmutableMap.of("k2", "v")) + .build(); + BlobInfo updatedBlob = storage.patch(toUpdateBlob); + assertNotNull(updatedBlob); + assertEquals(toUpdateBlob.bucket(), updatedBlob.bucket()); + assertEquals(toUpdateBlob.name(), updatedBlob.name()); + assertEquals(ImmutableMap.of("k1", "v", "k2", "v"), updatedBlob.metadata()); + assertTrue(storage.delete(bucket, blobName)); + } + + @Test + public void testPatchBlobFail() { + String blobName = "test-patch-blob-fail"; + BlobInfo blob = BlobInfo.builder(bucket, blobName).build(); + assertNotNull(storage.create(blob)); + try { + storage.patch(blob.toBuilder().contentType(CONTENT_TYPE).generation(-1L).build(), Storage.BlobTargetOption.generationMatch()); fail("StorageException was expected"); } catch (StorageException ex) { @@ -319,14 +353,17 @@ public void testCopyBlobFail() { public void testBatchRequest() { String sourceBlobName1 = "test-batch-request-blob-1"; String sourceBlobName2 = "test-batch-request-blob-2"; - BlobInfo sourceBlob1 = BlobInfo.builder(bucket, sourceBlobName1).build(); - BlobInfo sourceBlob2 = BlobInfo.builder(bucket, sourceBlobName2).build(); - assertNotNull(storage.create(sourceBlob1)); - assertNotNull(storage.create(sourceBlob2)); + BlobInfo sourceBlob1 = storage.create(BlobInfo.builder(bucket, sourceBlobName1).build()); + BlobInfo sourceBlob2 = storage.create(BlobInfo.builder(bucket, sourceBlobName2).build()); + assertNotNull(sourceBlob1); + assertNotNull(sourceBlob2); // Batch update request - BlobInfo updatedBlob1 = sourceBlob1.toBuilder().contentType(CONTENT_TYPE).build(); - BlobInfo updatedBlob2 = sourceBlob2.toBuilder().contentType(CONTENT_TYPE).build(); + ImmutableMap metadata = ImmutableMap.of("k1", "v1"); + BlobInfo updatedBlob1 = sourceBlob1.toBuilder() + .contentType(CONTENT_TYPE).metadata(metadata).build(); + BlobInfo updatedBlob2 = sourceBlob2.toBuilder().contentType(CONTENT_TYPE) + .contentType(CONTENT_TYPE).metadata(metadata).build(); BatchRequest updateRequest = BatchRequest.builder() .update(updatedBlob1) .update(updatedBlob2) @@ -335,14 +372,15 @@ public void testBatchRequest() { assertEquals(2, updateResponse.updates().size()); assertEquals(0, updateResponse.deletes().size()); assertEquals(0, updateResponse.gets().size()); + assertEquals(0, updateResponse.patches().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(updatedBlob1.blobId(), remoteUpdatedBlob1.blobId()); + assertEquals(updatedBlob2.blobId(), remoteUpdatedBlob2.blobId()); assertEquals(updatedBlob1.contentType(), remoteUpdatedBlob1.contentType()); assertEquals(updatedBlob2.contentType(), remoteUpdatedBlob2.contentType()); + assertEquals(metadata, remoteUpdatedBlob1.metadata()); + assertEquals(metadata, remoteUpdatedBlob2.metadata()); // Batch get request BatchRequest getRequest = BatchRequest.builder() @@ -353,11 +391,30 @@ public void testBatchRequest() { assertEquals(2, getResponse.gets().size()); assertEquals(0, getResponse.deletes().size()); assertEquals(0, getResponse.updates().size()); + assertEquals(0, getResponse.patches().size()); BlobInfo remoteBlob1 = getResponse.gets().get(0).get(); BlobInfo remoteBlob2 = getResponse.gets().get(1).get(); assertEquals(remoteUpdatedBlob1, remoteBlob1); assertEquals(remoteUpdatedBlob2, remoteBlob2); + // Batch patch request + ImmutableMap expectedMetadata = ImmutableMap.of("k1", "v1", "k2", "v2"); + ImmutableMap patchMetadata = ImmutableMap.of("k2", "v2"); + BatchRequest patchRequest = BatchRequest.builder() + .patch(remoteBlob1.toBuilder().metadata(patchMetadata).build()) + .patch(remoteBlob2.toBuilder().metadata(patchMetadata).build()) + .build(); + BatchResponse patchResponse = storage.apply(patchRequest); + assertEquals(0, patchResponse.gets().size()); + assertEquals(0, patchResponse.deletes().size()); + assertEquals(0, patchResponse.updates().size()); + assertEquals(2, patchResponse.patches().size()); + BlobInfo patchedBlob1 = patchResponse.patches().get(0).get(); + BlobInfo patchedBlob2 = patchResponse.patches().get(1).get(); + assertEquals(remoteBlob1.blobId(), patchedBlob1.blobId()); + assertEquals(expectedMetadata, patchedBlob1.metadata()); + assertEquals(expectedMetadata, patchedBlob2.metadata()); + // Batch delete request BatchRequest deleteRequest = BatchRequest.builder() .delete(bucket, sourceBlobName1) @@ -381,14 +438,17 @@ public void testBatchRequestFail() { .update(updatedBlob, Storage.BlobTargetOption.generationMatch()) .delete(bucket, blobName, Storage.BlobSourceOption.generationMatch(-1L)) .get(bucket, blobName, Storage.BlobSourceOption.generationMatch(-1L)) + .patch(updatedBlob, Storage.BlobTargetOption.generationMatch()) .build(); - BatchResponse updateResponse = storage.apply(batchRequest); - assertEquals(1, updateResponse.updates().size()); - assertEquals(1, updateResponse.deletes().size()); - assertEquals(1, updateResponse.gets().size()); - assertTrue(updateResponse.updates().get(0).failed()); - assertTrue(updateResponse.gets().get(0).failed()); - assertTrue(updateResponse.deletes().get(0).failed()); + BatchResponse batchResponse = storage.apply(batchRequest); + assertEquals(1, batchResponse.updates().size()); + assertEquals(1, batchResponse.deletes().size()); + assertEquals(1, batchResponse.gets().size()); + assertEquals(1, batchResponse.patches().size()); + assertTrue(batchResponse.updates().get(0).failed()); + assertTrue(batchResponse.gets().get(0).failed()); + assertTrue(batchResponse.deletes().get(0).failed()); + assertTrue(batchResponse.patches().get(0).failed()); assertTrue(storage.delete(bucket, blobName)); } @@ -529,10 +589,8 @@ public void testGetBlobs() { 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()); + assertEquals(sourceBlob1.blobId(), remoteInfos.get(0).blobId()); + assertEquals(sourceBlob2.blobId(), remoteInfos.get(1).blobId()); assertTrue(storage.delete(bucket, sourceBlobName1)); assertTrue(storage.delete(bucket, sourceBlobName2)); } @@ -545,8 +603,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 +646,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,10 +665,53 @@ 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)); } + + @Test + public void testPatchBlobs() { + String sourceBlobName1 = "test-patch-blobs-1"; + String sourceBlobName2 = "test-patch-blobs-2"; + ImmutableMap metadata = ImmutableMap.of("k1", "v1"); + ImmutableMap patchMetadata = ImmutableMap.of("k2", "v2"); + ImmutableMap expectedMetadata = ImmutableMap.of("k1", "v1", "k2", "v2"); + BlobInfo sourceBlob1 = BlobInfo.builder(bucket, sourceBlobName1).metadata(metadata).build(); + BlobInfo sourceBlob2 = BlobInfo.builder(bucket, sourceBlobName2).metadata(metadata).build(); + assertNotNull(storage.create(sourceBlob1)); + assertNotNull(storage.create(sourceBlob2)); + List patchedBlobs = storage.patch( + sourceBlob1.toBuilder().contentType(CONTENT_TYPE).metadata(patchMetadata).build(), + sourceBlob2.toBuilder().contentType(CONTENT_TYPE).metadata(patchMetadata).build()); + assertEquals(sourceBlob1.blobId(), patchedBlobs.get(0).blobId()); + assertEquals(CONTENT_TYPE, patchedBlobs.get(0).contentType()); + assertEquals(expectedMetadata, patchedBlobs.get(0).metadata()); + assertEquals(sourceBlob2.blobId(), patchedBlobs.get(1).blobId()); + assertEquals(CONTENT_TYPE, patchedBlobs.get(1).contentType()); + assertEquals(expectedMetadata, patchedBlobs.get(1).metadata()); + assertTrue(storage.delete(bucket, sourceBlobName1)); + assertTrue(storage.delete(bucket, sourceBlobName2)); + } + + @Test + public void testPatchBlobsFail() { + String sourceBlobName1 = "test-patch-blobs-fail-1"; + String sourceBlobName2 = "test-patch-blobs-fail-2"; + ImmutableMap metadata = ImmutableMap.of("k1", "v1"); + ImmutableMap patchMetadata = ImmutableMap.of("k2", "v2"); + ImmutableMap expectedMetadata = ImmutableMap.of("k1", "v1", "k2", "v2"); + BlobInfo sourceBlob1 = BlobInfo.builder(bucket, sourceBlobName1).metadata(metadata).build(); + BlobInfo sourceBlob2 = BlobInfo.builder(bucket, sourceBlobName2).metadata(metadata).build(); + assertNotNull(storage.create(sourceBlob1)); + List patchedBlobs = storage.patch( + sourceBlob1.toBuilder().contentType(CONTENT_TYPE).metadata(patchMetadata).build(), + sourceBlob2.toBuilder().contentType(CONTENT_TYPE).metadata(patchMetadata).build()); + assertEquals(sourceBlob1.blobId(), patchedBlobs.get(0).blobId()); + assertEquals(CONTENT_TYPE, patchedBlobs.get(0).contentType()); + assertEquals(expectedMetadata, patchedBlobs.get(0).metadata()); + assertNull(patchedBlobs.get(1)); + assertTrue(storage.delete(bucket, sourceBlobName1)); + } } diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/SerializationTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/SerializationTest.java index edda4ed17e25..5a13b0798453 100644 --- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/SerializationTest.java +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/SerializationTest.java @@ -53,7 +53,8 @@ public class SerializationTest { private static final BatchResponse BATCH_RESPONSE = new BatchResponse( Collections.singletonList(BatchResponse.Result.of(true)), Collections.>emptyList(), - Collections.>emptyList()); + Collections.>emptyList(), + Collections.singletonList(BatchResponse.Result.of(BLOB_INFO))); private static final BaseListResult LIST_RESULT = new BaseListResult<>(null, "c", Collections.singletonList(BlobInfo.builder("b", "n").build())); private static final Storage.BlobListOption BLOB_LIST_OPTIONS = diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java index b3a6fe36859e..fe0b0389fecc 100644 --- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java @@ -90,6 +90,7 @@ public class StorageImplTest { .metageneration(42L).generation(24L).contentType("application/json").md5("md5string").build(); private static final BlobInfo BLOB_INFO2 = BlobInfo.builder(BUCKET_NAME1, BLOB_NAME2).build(); private static final BlobInfo BLOB_INFO3 = BlobInfo.builder(BUCKET_NAME1, BLOB_NAME3).build(); + private static final BlobInfo BLOB_INFO4 = BlobInfo.builder(BUCKET_NAME2, BLOB_NAME1).build(); // Empty StorageRpc options private static final Map EMPTY_RPC_OPTIONS = ImmutableMap.of(); @@ -526,7 +527,7 @@ public void testUpdateBlob() { BlobInfo updatedBlobInfo = BLOB_INFO1.toBuilder().contentType("some-content-type").build(); EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); EasyMock.expect(optionsMock.retryParams()).andReturn(RetryParams.noRetries()); - EasyMock.expect(storageRpcMock.patch(updatedBlobInfo.toPb(), EMPTY_RPC_OPTIONS)) + EasyMock.expect(storageRpcMock.update(updatedBlobInfo.toPb(), EMPTY_RPC_OPTIONS)) .andReturn(updatedBlobInfo.toPb()); EasyMock.replay(optionsMock, storageRpcMock); storage = StorageFactory.instance().get(optionsMock); @@ -539,7 +540,7 @@ public void testUpdateBlobWithOptions() { BlobInfo updatedBlobInfo = BLOB_INFO1.toBuilder().contentType("some-content-type").build(); EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); EasyMock.expect(optionsMock.retryParams()).andReturn(RetryParams.noRetries()); - EasyMock.expect(storageRpcMock.patch(updatedBlobInfo.toPb(), BLOB_TARGET_OPTIONS_UPDATE)) + EasyMock.expect(storageRpcMock.update(updatedBlobInfo.toPb(), BLOB_TARGET_OPTIONS_UPDATE)) .andReturn(updatedBlobInfo.toPb()); EasyMock.replay(optionsMock, storageRpcMock); storage = StorageFactory.instance().get(optionsMock); @@ -548,6 +549,33 @@ public void testUpdateBlobWithOptions() { assertEquals(updatedBlobInfo, blob); } + @Test + public void testPatchBlob() { + BlobInfo updatedBlobInfo = BLOB_INFO1.toBuilder().contentType("some-content-type").build(); + EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); + EasyMock.expect(optionsMock.retryParams()).andReturn(RetryParams.noRetries()); + EasyMock.expect(storageRpcMock.patch(updatedBlobInfo.toPb(), EMPTY_RPC_OPTIONS)) + .andReturn(updatedBlobInfo.toPb()); + EasyMock.replay(optionsMock, storageRpcMock); + storage = StorageFactory.instance().get(optionsMock); + BlobInfo blob = storage.patch(updatedBlobInfo); + assertEquals(updatedBlobInfo, blob); + } + + @Test + public void testPatchBlobWithOptions() { + BlobInfo updatedBlobInfo = BLOB_INFO1.toBuilder().contentType("some-content-type").build(); + EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); + EasyMock.expect(optionsMock.retryParams()).andReturn(RetryParams.noRetries()); + EasyMock.expect(storageRpcMock.patch(updatedBlobInfo.toPb(), BLOB_TARGET_OPTIONS_UPDATE)) + .andReturn(updatedBlobInfo.toPb()); + EasyMock.replay(optionsMock, storageRpcMock); + storage = StorageFactory.instance().get(optionsMock); + BlobInfo blob = + storage.patch(updatedBlobInfo, BLOB_TARGET_METAGENERATION, BLOB_TARGET_PREDEFINED_ACL); + assertEquals(updatedBlobInfo, blob); + } + @Test public void testDeleteBucket() { EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); @@ -697,16 +725,20 @@ public void testApply() { .delete(BUCKET_NAME1, BLOB_NAME1) .update(BLOB_INFO2) .get(BUCKET_NAME1, BLOB_NAME3) + .patch(BLOB_INFO4) .build(); List toDelete = ImmutableList.of(BlobId.of(BUCKET_NAME1, BLOB_NAME1).toPb()); List toUpdate = ImmutableList.of(BlobId.of(BUCKET_NAME1, BLOB_NAME2).toPb()); List toGet = ImmutableList.of(BlobId.of(BUCKET_NAME1, BLOB_NAME3).toPb()); + List toPatch = ImmutableList.of(BlobId.of(BUCKET_NAME2, BLOB_NAME1).toPb()); List> deleteOptions = ImmutableList.>of(EMPTY_RPC_OPTIONS); List> updateOptions = ImmutableList.>of(EMPTY_RPC_OPTIONS); List> getOptions = ImmutableList.>of(EMPTY_RPC_OPTIONS); + List> patchOptions = + ImmutableList.>of(EMPTY_RPC_OPTIONS); Map> deleteResult = Maps.toMap(toDelete, new Function>() { @@ -729,8 +761,15 @@ public Tuple apply(StorageObject f) { return Tuple.of(f, null); } }); + Map> patchResult = Maps.toMap(toPatch, + new Function>() { + @Override + public Tuple apply(StorageObject f) { + return Tuple.of(f, null); + } + }); StorageRpc.BatchResponse res = - new StorageRpc.BatchResponse(deleteResult, updateResult, getResult); + new StorageRpc.BatchResponse(deleteResult, updateResult, getResult, patchResult); EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); Capture capturedBatchRequest = Capture.newInstance(); @@ -746,6 +785,8 @@ public Tuple apply(StorageObject f) { capturedBatchRequest.getValue().toUpdate; List>> capturedToGet = capturedBatchRequest.getValue().toGet; + List>> capturedToPatch = + capturedBatchRequest.getValue().toPatch; for (int i = 0; i < capturedToDelete.size(); i++) { assertEquals(toDelete.get(i), capturedToDelete.get(i).x()); assertEquals(deleteOptions.get(i), capturedToDelete.get(i).y()); @@ -758,6 +799,10 @@ public Tuple apply(StorageObject f) { assertEquals(toGet.get(i), capturedToGet.get(i).x()); assertEquals(getOptions.get(i), capturedToGet.get(i).y()); } + for (int i = 0; i < capturedToPatch.size(); i++) { + assertEquals(toPatch.get(i), capturedToPatch.get(i).x()); + assertEquals(patchOptions.get(i), capturedToPatch.get(i).y()); + } // Verify BatchResponse for (BatchResponse.Result result : batchResponse.deletes()) { @@ -769,6 +814,9 @@ public Tuple apply(StorageObject f) { for (int i = 0; i < batchResponse.gets().size(); i++) { assertEquals(toGet.get(i), batchResponse.gets().get(i).get().toPb()); } + for (int i = 0; i < batchResponse.patches().size(); i++) { + assertEquals(toPatch.get(i), batchResponse.patches().get(i).get().toPb()); + } } @Test @@ -917,8 +965,9 @@ public Tuple apply(StorageObject f) { return Tuple.of(f, null); } }); + Map> patchResult = ImmutableMap.of(); StorageRpc.BatchResponse res = - new StorageRpc.BatchResponse(deleteResult, updateResult, getResult); + new StorageRpc.BatchResponse(deleteResult, updateResult, getResult, patchResult); EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); Capture capturedBatchRequest = Capture.newInstance(); @@ -932,6 +981,7 @@ public Tuple apply(StorageObject f) { capturedBatchRequest.getValue().toGet; assertTrue(capturedBatchRequest.getValue().toDelete.isEmpty()); assertTrue(capturedBatchRequest.getValue().toUpdate.isEmpty()); + assertTrue(capturedBatchRequest.getValue().toPatch.isEmpty()); for (int i = 0; i < capturedToGet.size(); i++) { assertEquals(toGet.get(i), capturedToGet.get(i).x()); assertTrue(capturedToGet.get(i).y().isEmpty()); @@ -960,8 +1010,9 @@ public Tuple apply(StorageObject f) { return Tuple.of(f, null); } }); + Map> patchResult = ImmutableMap.of(); StorageRpc.BatchResponse res = - new StorageRpc.BatchResponse(deleteResult, updateResult, getResult); + new StorageRpc.BatchResponse(deleteResult, updateResult, getResult, patchResult); EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); Capture capturedBatchRequest = Capture.newInstance(); @@ -975,6 +1026,7 @@ public Tuple apply(StorageObject f) { capturedBatchRequest.getValue().toUpdate; assertTrue(capturedBatchRequest.getValue().toDelete.isEmpty()); assertTrue(capturedBatchRequest.getValue().toGet.isEmpty()); + assertTrue(capturedBatchRequest.getValue().toPatch.isEmpty()); for (int i = 0; i < capturedToUpdate.size(); i++) { assertEquals(toUpdate.get(i), capturedToUpdate.get(i).x()); assertTrue(capturedToUpdate.get(i).y().isEmpty()); @@ -1003,8 +1055,9 @@ public Tuple apply(StorageObject f) { return Tuple.of(true, null); } }); + Map> patchResult = ImmutableMap.of(); StorageRpc.BatchResponse res = - new StorageRpc.BatchResponse(deleteResult, updateResult, getResult); + new StorageRpc.BatchResponse(deleteResult, updateResult, getResult, patchResult); EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); Capture capturedBatchRequest = Capture.newInstance(); @@ -1018,6 +1071,7 @@ public Tuple apply(StorageObject f) { capturedBatchRequest.getValue().toDelete; assertTrue(capturedBatchRequest.getValue().toUpdate.isEmpty()); assertTrue(capturedBatchRequest.getValue().toGet.isEmpty()); + assertTrue(capturedBatchRequest.getValue().toPatch.isEmpty()); for (int i = 0; i < capturedToDelete.size(); i++) { assertEquals(toUpdate.get(i), capturedToDelete.get(i).x()); assertTrue(capturedToDelete.get(i).y().isEmpty()); @@ -1029,6 +1083,51 @@ public Tuple apply(StorageObject f) { } } + @Test + public void testPatchAll() { + BlobInfo blobInfo1 = BlobInfo.builder(BUCKET_NAME1, BLOB_NAME1).contentType("type").build(); + BlobInfo blobInfo2 = BlobInfo.builder(BUCKET_NAME1, BLOB_NAME2).contentType("type").build(); + StorageObject storageObject1 = blobInfo1.toPb(); + StorageObject storageObject2 = blobInfo2.toPb(); + List toPatch = ImmutableList.of(storageObject1, storageObject2); + + Map> deleteResult = ImmutableMap.of(); + Map> getResult = ImmutableMap.of(); + Map> updateResult = ImmutableMap.of(); + Map> patchResult = Maps.toMap(toPatch, + new Function>() { + @Override + public Tuple apply(StorageObject f) { + return Tuple.of(f, null); + } + }); + StorageRpc.BatchResponse res = + new StorageRpc.BatchResponse(deleteResult, updateResult, getResult, patchResult); + + EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock); + Capture capturedBatchRequest = Capture.newInstance(); + EasyMock.expect(storageRpcMock.batch(EasyMock.capture(capturedBatchRequest))).andReturn(res); + EasyMock.replay(optionsMock, storageRpcMock); + storage = StorageFactory.instance().get(optionsMock); + List resultBlobs = storage.patch(blobInfo1, blobInfo2); + + // Verify captured StorageRpc.BatchRequest + List>> capturedToPatch = + capturedBatchRequest.getValue().toPatch; + assertTrue(capturedBatchRequest.getValue().toDelete.isEmpty()); + assertTrue(capturedBatchRequest.getValue().toGet.isEmpty()); + assertTrue(capturedBatchRequest.getValue().toUpdate.isEmpty()); + for (int i = 0; i < capturedToPatch.size(); i++) { + assertEquals(toPatch.get(i), capturedToPatch.get(i).x()); + assertTrue(capturedToPatch.get(i).y().isEmpty()); + } + + // Verify result + for (int i = 0; i < resultBlobs.size(); i++) { + assertEquals(toPatch.get(i), resultBlobs.get(i).toPb()); + } + } + @Test public void testRetryableException() { BlobId blob = BlobId.of(BUCKET_NAME1, BLOB_NAME1); From f2a687871deb71b6606b2089b8819290606daab9 Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Wed, 21 Oct 2015 18:05:14 +0200 Subject: [PATCH 2/4] Add patch (static and non) method to Blob, add tests --- .../java/com/google/gcloud/storage/Blob.java | 84 +++++++++++++++---- .../com/google/gcloud/storage/BlobTest.java | 49 +++++++++++ 2 files changed, 119 insertions(+), 14 deletions(-) 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..fcfa2d7e9d33 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 @@ -184,12 +184,14 @@ public Blob reload(BlobSourceOption... options) { } /** - * Updates the blob's information. Bucket or blob's name cannot be changed by this method. If you - * want to rename the blob or move it to a different bucket use the {@link #copyTo} and - * {@link #delete} operations. A new {@code Blob} object is returned. By default no checks are - * 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())}. + * Updates the blob's information, metadata are replaced. Bucket or blob's name cannot be changed + * by this method. If you want to rename the blob or move it to a different bucket use the + * {@link #copyTo} and {@link #delete} operations. A new {@code Blob} object is returned. By + * default no checks are 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())}. + * Update will fail if an incomplete {@code blobInfo} is provided. * * @param blobInfo new blob's information. Bucket and blob names must match the current ones * @param options update options @@ -202,6 +204,32 @@ public Blob update(BlobInfo blobInfo, BlobTargetOption... options) { return new Blob(storage, storage.update(blobInfo, options)); } + /** + * Updates the blob's information according to patch semantics. Original metadata are merged with + * metadata in the provided {@code blobInfo}. To replace metadata use + * {@link #update(com.google.gcloud.storage.BlobInfo, + * com.google.gcloud.storage.Storage.BlobTargetOption...)} instead. Bucket or blob's name cannot + * be changed by this method. If you want to rename the blob or move it to a different bucket use + * the {@link #copyTo} and {@link #delete} operations. A new {@code Blob} object is returned. + * + * By default no checks are 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.patch(newInfo, BlobTargetOption.metagenerationMatch())}. + * + * @param blobInfo updated blob's information. Bucket and blob names must match the current ones + * @param options update options + * @return a {@code Blob} object with patched information + * @throws StorageException upon failure + * @see + * Patch (partial update) + */ + public Blob patch(BlobInfo blobInfo, BlobTargetOption... options) { + checkArgument(Objects.equals(blobInfo.bucket(), info.bucket()), "Bucket name must match"); + checkArgument(Objects.equals(blobInfo.name(), info.name()), "Blob name must match"); + return new Blob(storage, storage.patch(blobInfo, options)); + } + /** * Copies this blob to the specified target. Possibly copying also some of the metadata * (e.g. content-type). @@ -306,8 +334,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 get blobs. * * @param storage the storage service used to issue the request * @param blobs the blobs to get @@ -331,13 +358,14 @@ 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 using a batch request. Blobs metadata will be replaced using values + * from the given {@code BlobInfo} objects. * * @param storage the storage service used to issue the request * @param infos the blobs to update - * @return an immutable list of {@code Blob} objects. If a blob does not exist or access to it has - * been denied the corresponding item in the list is {@code null}. + * @return an immutable list of {@code Blob} objects. If an incomplete {@code blobInfo} is + * provided, the blob does not exist or access to it has been denied the corresponding item in + * the list is {@code null}. * @throws StorageException upon failure */ public static List update(final Storage storage, BlobInfo... infos) { @@ -356,8 +384,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 using a batch request. * * @param storage the storage service used to issue the request * @param blobs the blobs to delete @@ -374,4 +401,33 @@ public static List delete(Storage storage, BlobId... blobs) { } return storage.delete(blobs); } + + /** + * Updates the requested blobs according to the patch semantics. Original metadata are merged with + * metadata in the provided {@code BlobInfo} objects. To replace metadata use {@link + * #update(com.google.gcloud.storage.Storage, com.google.gcloud.storage.BlobInfo...)} instead. + * A batch request is used to perform this call. + * + * @param storage the storage service used to issue the request + * @param infos the blobs to update + * @return an immutable list of {@code Blob} objects. If a blob does not exist or access to it has + * been denied the corresponding item in the list is {@code null}. + * @throws StorageException upon failure + * @see + * Patch (partial update) + */ + public static List patch(final Storage storage, BlobInfo... infos) { + checkNotNull(storage); + checkNotNull(infos); + if (infos.length == 0) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(Lists.transform(storage.patch(infos), + new Function() { + @Override + public Blob apply(BlobInfo f) { + return f != null ? new Blob(storage, f) : null; + } + })); + } } diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java index dddbb763f04c..079f0a32945b 100644 --- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java @@ -110,6 +110,16 @@ public void testUpdate() throws Exception { assertEquals(updatedInfo, updatedBlob.info()); } + @Test + public void testPatch() throws Exception { + BlobInfo updatedInfo = BLOB_INFO.toBuilder().cacheControl("c").build(); + expect(storage.patch(updatedInfo, new Storage.BlobTargetOption[0])).andReturn(updatedInfo); + replay(storage); + Blob updatedBlob = blob.patch(updatedInfo); + assertSame(storage, blob.storage()); + assertEquals(updatedInfo, updatedBlob.info()); + } + @Test public void testDelete() throws Exception { expect(storage.delete(BLOB_INFO.blobId(), new Storage.BlobSourceOption[0])).andReturn(true); @@ -272,6 +282,45 @@ public void testDeleteSome() throws Exception { } } + @Test + public void testPatchNone() throws Exception { + replay(storage); + assertTrue(Blob.patch(storage).isEmpty()); + } + + @Test + public void testPatchSome() throws Exception { + List blobInfoList = Lists.newArrayListWithCapacity(BLOB_ID_ARRAY.length); + for (BlobInfo info : BLOB_INFO_ARRAY) { + blobInfoList.add(info.toBuilder().contentType("content").build()); + } + expect(storage.patch(BLOB_INFO_ARRAY)).andReturn(blobInfoList); + replay(storage); + List result = Blob.patch(storage, BLOB_INFO_ARRAY); + assertEquals(blobInfoList.size(), result.size()); + for (int i = 0; i < blobInfoList.size(); i++) { + assertEquals(blobInfoList.get(i), result.get(i).info()); + } + } + + @Test + public void testPatchSomeNull() throws Exception { + List blobInfoList = Arrays.asList( + BLOB_INFO_ARRAY[0].toBuilder().contentType("content").build(), null, + BLOB_INFO_ARRAY[2].toBuilder().contentType("content").build()); + expect(storage.patch(BLOB_INFO_ARRAY)).andReturn(blobInfoList); + replay(storage); + List result = Blob.patch(storage, BLOB_INFO_ARRAY); + assertEquals(blobInfoList.size(), result.size()); + for (int i = 0; i < blobInfoList.size(); i++) { + if (blobInfoList.get(i) != null) { + assertEquals(blobInfoList.get(i), result.get(i).info()); + } else { + assertNull(result.get(i)); + } + } + } + @Test public void testLoadFromString() throws Exception { expect(storage.get(BLOB_INFO.blobId())).andReturn(BLOB_INFO); From c80f5a0cf8331659db27df095d4908027ab074f5 Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Thu, 22 Oct 2015 14:50:33 +0200 Subject: [PATCH 3/4] Move patch requests ahead of gets in batch --- .../com/google/gcloud/spi/DefaultStorageRpc.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java index 5b89d170fedf..6701c1306863 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java @@ -426,32 +426,32 @@ public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { } }); } - for (final Tuple> tuple : request.toGet) { - getRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { + for (final Tuple> tuple : request.toPatch) { + patchRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { @Override public void onSuccess(StorageObject storageObject, HttpHeaders responseHeaders) { - gets.put(tuple.x(), + patches.put(tuple.x(), Tuple.of(storageObject, null)); } @Override public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { - gets.put(tuple.x(), + patches.put(tuple.x(), Tuple.of(null, translate(e))); } }); } - for (final Tuple> tuple : request.toPatch) { - patchRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { + for (final Tuple> tuple : request.toGet) { + getRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { @Override public void onSuccess(StorageObject storageObject, HttpHeaders responseHeaders) { - patches.put(tuple.x(), + gets.put(tuple.x(), Tuple.of(storageObject, null)); } @Override public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { - patches.put(tuple.x(), + gets.put(tuple.x(), Tuple.of(null, translate(e))); } }); From 96702f8217ce0b8f9ad648a419d6b3eb7e4242c4 Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Thu, 22 Oct 2015 14:51:09 +0200 Subject: [PATCH 4/4] Fix error in BatchResponse.equals --- .../src/main/java/com/google/gcloud/storage/BatchResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java index 7a5dc4464b76..f401e2f3682a 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java @@ -135,7 +135,7 @@ public boolean equals(Object obj) { BatchResponse other = (BatchResponse) obj; return Objects.equals(deleteResult, other.deleteResult) && Objects.equals(updateResult, other.updateResult) - && Objects.equals(updateResult, other.updateResult) + && Objects.equals(getResult, other.getResult) && Objects.equals(patchResult, other.patchResult); }