From 04c9c6afd251de971248997d99506b981cead728 Mon Sep 17 00:00:00 2001 From: Andrew Gaul Date: Tue, 8 Oct 2013 15:51:32 -0700 Subject: [PATCH] JCLOUDS-339. Handle zero-length objects in Atmos Atmos does not return a location header when writing zero-length objects, which normally throws an HttpResponseException: no uri in headers or content. --- .../org/jclouds/atmos/AtmosAsyncClient.java | 4 ++ .../java/org/jclouds/atmos/AtmosClient.java | 2 + ...lableURIFromListOrLocationHeaderIf20x.java | 43 +++++++++++++++++++ .../jclouds/atmos/AtmosAsyncClientTest.java | 5 ++- .../jclouds/atmos/AtmosClientLiveTest.java | 10 +++++ ...ParseURIFromListOrLocationHeaderIf20x.java | 2 +- 6 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 apis/atmos/src/main/java/org/jclouds/atmos/functions/ParseNullableURIFromListOrLocationHeaderIf20x.java diff --git a/apis/atmos/src/main/java/org/jclouds/atmos/AtmosAsyncClient.java b/apis/atmos/src/main/java/org/jclouds/atmos/AtmosAsyncClient.java index 355e0d5def..91435ea8a9 100644 --- a/apis/atmos/src/main/java/org/jclouds/atmos/AtmosAsyncClient.java +++ b/apis/atmos/src/main/java/org/jclouds/atmos/AtmosAsyncClient.java @@ -45,6 +45,7 @@ import org.jclouds.atmos.domain.UserMetadata; import org.jclouds.atmos.filters.SignRequest; import org.jclouds.atmos.functions.AtmosObjectName; import org.jclouds.atmos.functions.ParseDirectoryListFromContentAndHeaders; +import org.jclouds.atmos.functions.ParseNullableURIFromListOrLocationHeaderIf20x; import org.jclouds.atmos.functions.ParseObjectFromHeadersAndHttpContent; import org.jclouds.atmos.functions.ParseSystemMetadataFromHeaders; import org.jclouds.atmos.functions.ParseUserMetadataFromHeaders; @@ -55,6 +56,7 @@ import org.jclouds.blobstore.BlobStoreFallbacks.NullOnKeyAlreadyExists; import org.jclouds.blobstore.BlobStoreFallbacks.ThrowContainerNotFoundOn404; import org.jclouds.blobstore.BlobStoreFallbacks.ThrowKeyNotFoundOn404; import org.jclouds.http.options.GetOptions; +import org.jclouds.javax.annotation.Nullable; import org.jclouds.rest.annotations.BinderParam; import org.jclouds.rest.annotations.Fallback; import org.jclouds.rest.annotations.Headers; @@ -123,10 +125,12 @@ public interface AtmosAsyncClient extends Closeable { /** * @see AtmosClient#createFile */ + @Nullable @Named("CreateObject") @POST @Path("/{parent}/{name}") @Headers(keys = EXPECT, values = "100-continue") + @ResponseParser(ParseNullableURIFromListOrLocationHeaderIf20x.class) @Consumes(MediaType.WILDCARD) ListenableFuture createFile( @PathParam("parent") String parent, diff --git a/apis/atmos/src/main/java/org/jclouds/atmos/AtmosClient.java b/apis/atmos/src/main/java/org/jclouds/atmos/AtmosClient.java index 8490fc5c61..df9396bb86 100644 --- a/apis/atmos/src/main/java/org/jclouds/atmos/AtmosClient.java +++ b/apis/atmos/src/main/java/org/jclouds/atmos/AtmosClient.java @@ -26,6 +26,7 @@ import org.jclouds.atmos.domain.UserMetadata; import org.jclouds.atmos.options.ListOptions; import org.jclouds.atmos.options.PutOptions; import org.jclouds.http.options.GetOptions; +import org.jclouds.javax.annotation.Nullable; import com.google.inject.Provides; @@ -50,6 +51,7 @@ public interface AtmosClient extends Closeable { URI createDirectory(String directoryName, PutOptions... options); + @Nullable URI createFile(String parent, AtmosObject object, PutOptions... options); void updateFile(String parent, AtmosObject object, PutOptions... options); diff --git a/apis/atmos/src/main/java/org/jclouds/atmos/functions/ParseNullableURIFromListOrLocationHeaderIf20x.java b/apis/atmos/src/main/java/org/jclouds/atmos/functions/ParseNullableURIFromListOrLocationHeaderIf20x.java new file mode 100644 index 0000000000..633be0c176 --- /dev/null +++ b/apis/atmos/src/main/java/org/jclouds/atmos/functions/ParseNullableURIFromListOrLocationHeaderIf20x.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.atmos.functions; + +import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; +import static org.jclouds.http.HttpUtils.releasePayload; + +import java.net.URI; + +import org.jclouds.http.HttpResponse; +import org.jclouds.http.functions.ParseURIFromListOrLocationHeaderIf20x; + +/** + * Parses a single URI from a list, returning null when blob length was zero. + * Atmos returns "HTTP/1.1 201 null" when putting zero-length blobs. + * + * @author Andrew Gaul + */ +public class ParseNullableURIFromListOrLocationHeaderIf20x extends ParseURIFromListOrLocationHeaderIf20x { + + @Override + public URI apply(HttpResponse from) { + if (from.getStatusCode() == 201 && request.getPayload().getContentMetadata().getContentLength() == 0) { + releasePayload(from); + return null; + } + return super.apply(from); + } +} diff --git a/apis/atmos/src/test/java/org/jclouds/atmos/AtmosAsyncClientTest.java b/apis/atmos/src/test/java/org/jclouds/atmos/AtmosAsyncClientTest.java index 8287417072..83f2985351 100644 --- a/apis/atmos/src/test/java/org/jclouds/atmos/AtmosAsyncClientTest.java +++ b/apis/atmos/src/test/java/org/jclouds/atmos/AtmosAsyncClientTest.java @@ -33,6 +33,7 @@ import org.jclouds.atmos.domain.AtmosObject; import org.jclouds.atmos.fallbacks.EndpointIfAlreadyExists; import org.jclouds.atmos.filters.SignRequest; import org.jclouds.atmos.functions.ParseDirectoryListFromContentAndHeaders; +import org.jclouds.atmos.functions.ParseNullableURIFromListOrLocationHeaderIf20x; import org.jclouds.atmos.functions.ParseObjectFromHeadersAndHttpContent; import org.jclouds.atmos.functions.ParseSystemMetadataFromHeaders; import org.jclouds.atmos.functions.ReturnTrueIfGroupACLIsOtherRead; @@ -169,7 +170,7 @@ public class AtmosAsyncClientTest extends BaseAsyncClientTest assertNonPayloadHeadersEqual(request, HttpHeaders.ACCEPT + ": */*\nExpect: 100-continue\n"); assertPayloadEquals(request, "hello", "text/plain", false); - assertResponseParserClassEquals(method, request, ParseURIFromListOrLocationHeaderIf20x.class); + assertResponseParserClassEquals(method, request, ParseNullableURIFromListOrLocationHeaderIf20x.class); assertSaxResponseParserClassEquals(method, null); assertFallbackClassEquals(method, null); @@ -187,7 +188,7 @@ public class AtmosAsyncClientTest extends BaseAsyncClientTest + ": */*\nExpect: 100-continue\nx-emc-groupacl: other=READ\nx-emc-useracl: root=FULL_CONTROL\n"); assertPayloadEquals(request, "hello", "text/plain", false); - assertResponseParserClassEquals(method, request, ParseURIFromListOrLocationHeaderIf20x.class); + assertResponseParserClassEquals(method, request, ParseNullableURIFromListOrLocationHeaderIf20x.class); assertSaxResponseParserClassEquals(method, null); assertFallbackClassEquals(method, null); diff --git a/apis/atmos/src/test/java/org/jclouds/atmos/AtmosClientLiveTest.java b/apis/atmos/src/test/java/org/jclouds/atmos/AtmosClientLiveTest.java index 3d5f45902a..ce9a07f620 100644 --- a/apis/atmos/src/test/java/org/jclouds/atmos/AtmosClientLiveTest.java +++ b/apis/atmos/src/test/java/org/jclouds/atmos/AtmosClientLiveTest.java @@ -188,6 +188,16 @@ public class AtmosClientLiveTest extends BaseBlobStoreIntegrationTest { } } + @Test(timeOut = 5 * 60 * 1000, dependsOnMethods = { "testFileOperations" }) + public void testPutZeroLengthBlob() throws Exception { + AtmosObject object = getApi().newObject(); + object.getContentMetadata().setName("object"); + byte[] payload = new byte[0]; + object.setPayload(Payloads.newPayload(payload)); + object.getContentMetadata().setContentLength(Long.valueOf(payload.length)); + replaceObject(object); + } + private void createOrUpdateWithErrorLoop(boolean stream, String data, String metadataValue) throws Exception { createOrReplaceObject("object", makeData(data, stream), metadataValue); assertEventuallyObjectMatches("object", data, metadataValue); diff --git a/core/src/main/java/org/jclouds/http/functions/ParseURIFromListOrLocationHeaderIf20x.java b/core/src/main/java/org/jclouds/http/functions/ParseURIFromListOrLocationHeaderIf20x.java index a3f4d0c917..efe6596138 100644 --- a/core/src/main/java/org/jclouds/http/functions/ParseURIFromListOrLocationHeaderIf20x.java +++ b/core/src/main/java/org/jclouds/http/functions/ParseURIFromListOrLocationHeaderIf20x.java @@ -42,7 +42,7 @@ import com.google.common.base.Function; public class ParseURIFromListOrLocationHeaderIf20x implements Function, InvocationContext { - private HttpRequest request; + protected HttpRequest request; public URI apply(HttpResponse from) { if (from.getStatusCode() > 206)