JCLOUDS-1264: Swift Unicode multipart manifests

This fixes a bug where previously BindManifestToJsonPayload used the
character length as the ContentLength, instead of the byte length,
which caused issues if the JSON contained multi-byte Unicode
characters.
This commit is contained in:
Mat Mannion 2017-04-03 12:19:21 +01:00 committed by Andrew Gaul
parent 04ab255d9f
commit d41101df59
5 changed files with 132 additions and 119 deletions

View File

@ -1,65 +0,0 @@
/*
* 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.openstack.swift.v1.binders;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Map;
import javax.inject.Inject;
import org.jclouds.http.HttpRequest;
import org.jclouds.json.Json;
import org.jclouds.rest.MapBinder;
/**
* Binds the object to the request as a json object.
*/
public class BindManifestToJsonPayload implements MapBinder {
protected final Json jsonBinder;
@Inject
BindManifestToJsonPayload(Json jsonBinder) {
this.jsonBinder = jsonBinder;
}
@Override
public <R extends HttpRequest> R bindToRequest(R request, Map<String, Object> postParams) {
return bindToRequest(request, (Object) postParams);
}
@Override
public <R extends HttpRequest> R bindToRequest(R request, Object payload) {
String json = jsonBinder.toJson(checkNotNull(payload, "payload"));
request.setPayload(json);
/**
* The Content-Length request header must contain the length of the JSON content, not the length of the segment
* objects. However, after the PUT operation is complete, the Content-Length metadata is set to the total length
* of all the object segments. A similar situation applies to the ETag header. If it is used in the PUT
* operation, it must contain the MD5 checksum of the JSON content. The ETag metadata value is then set to be
* the MD5 checksum of the concatenated ETag values of the object segments. You can also set the Content-Type
* request header and custom object metadata.
* When the PUT operation sees the ?multipart-manifest=put query string, it reads the request body and verifies
* that each segment object exists and that the sizes and ETags match. If there is a mismatch, the PUT operation
* fails.
*/
request.getPayload().getContentMetadata().setContentLength((long)json.length());
return request;
}
}

View File

@ -494,7 +494,7 @@ public class RegionScopedSwiftBlobStore implements BlobStore {
mapBuilder.put("content-type", contentMetadata.getContentType());
}
/**
* Do not set content-length. Set automatically to manifest json string length by BindManifestToJsonPayload
* Do not set content-length. Set automatically to manifest json string length by BindToJsonPayload
*/
if (contentMetadata.getContentDisposition() != null) {
mapBuilder.put("content-disposition", contentMetadata.getContentDisposition());

View File

@ -32,7 +32,6 @@ import javax.ws.rs.PathParam;
import org.jclouds.Fallbacks.EmptyListOnNotFoundOr404;
import org.jclouds.Fallbacks.VoidOnNotFoundOr404;
import org.jclouds.openstack.keystone.v2_0.filters.AuthenticateRequest;
import org.jclouds.openstack.swift.v1.binders.BindManifestToJsonPayload;
import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindObjectMetadataToHeaders;
import org.jclouds.openstack.swift.v1.binders.BindToHeaders;
import org.jclouds.openstack.swift.v1.domain.DeleteStaticLargeObjectResponse;
@ -101,7 +100,7 @@ public interface StaticLargeObjectApi {
@ResponseParser(ETagHeader.class)
@QueryParams(keys = "multipart-manifest", values = "put")
String replaceManifest(@PathParam("objectName") String objectName,
@BinderParam(BindManifestToJsonPayload.class) List<Segment> segments,
@BinderParam(BindToJsonPayload.class) List<Segment> segments,
@BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> metadata,
@BinderParam(BindToHeaders.class) Map<String, String> headers);

View File

@ -41,14 +41,16 @@ import com.google.common.io.ByteSource;
@Test(groups = "live", testName = "StaticLargeObjectApiLiveTest", singleThreaded = true)
public class StaticLargeObjectApiLiveTest extends BaseSwiftApiLiveTest {
private String name = getClass().getSimpleName();
private String containerName = getClass().getSimpleName() + "Container";
private String defaultName = getClass().getSimpleName();
private String defaultContainerName = getClass().getSimpleName() + "Container";
private String unicodeName = getClass().getSimpleName() + "unic₪de";
private String unicodeContainerName = getClass().getSimpleName() + "unic₪deContainer";
private byte[] megOf1s;
private byte[] megOf2s;
public void testNotPresentWhenDeleting() throws Exception {
for (String regionId : regions) {
DeleteStaticLargeObjectResponse resp = getApi().getStaticLargeObjectApi(regionId, containerName).delete(UUID.randomUUID().toString());
DeleteStaticLargeObjectResponse resp = getApi().getStaticLargeObjectApi(regionId, defaultContainerName).delete(UUID.randomUUID().toString());
assertThat(resp.status()).isEqualTo("200 OK");
assertThat(resp.deleted()).isZero();
assertThat(resp.notFound()).isEqualTo(1);
@ -59,69 +61,91 @@ public class StaticLargeObjectApiLiveTest extends BaseSwiftApiLiveTest {
@Test
public void testReplaceManifest() throws Exception {
for (String regionId : regions) {
ObjectApi objectApi = getApi().getObjectApi(regionId, containerName);
String etag1s = objectApi.put(name + "/1", newByteSourcePayload(ByteSource.wrap(megOf1s)));
awaitConsistency();
assertMegabyteAndETagMatches(regionId, name + "/1", etag1s);
String etag2s = objectApi.put(name + "/2", newByteSourcePayload(ByteSource.wrap(megOf2s)));
awaitConsistency();
assertMegabyteAndETagMatches(regionId, name + "/2", etag2s);
List<Segment> segments = ImmutableList.<Segment> builder()
.add(Segment.builder()
.path(format("%s/%s/1", containerName, name)).etag(etag1s).sizeBytes(1024 * 1024)
.build())
.add(Segment.builder()
.path(format("%s/%s/2", containerName, name)).etag(etag2s).sizeBytes(1024 * 1024)
.build())
.build();
awaitConsistency();
String etagOfEtags = getApi().getStaticLargeObjectApi(regionId, containerName).replaceManifest(
name, segments, ImmutableMap.of("myfoo", "Bar"));
assertNotNull(etagOfEtags);
awaitConsistency();
SwiftObject bigObject = getApi().getObjectApi(regionId, containerName).get(name);
assertEquals(bigObject.getETag(), etagOfEtags);
assertEquals(bigObject.getPayload().getContentMetadata().getContentLength(), Long.valueOf(2 * 1024 * 1024));
assertEquals(bigObject.getMetadata(), ImmutableMap.of("myfoo", "Bar"));
// segments are visible
assertEquals(getApi().getContainerApi(regionId).get(containerName).getObjectCount(), Long.valueOf(3));
assertReplaceManifest(regionId, defaultContainerName, defaultName);
}
}
@Test(dependsOnMethods = "testReplaceManifest")
public void testDelete() throws Exception {
for (String regionId : regions) {
DeleteStaticLargeObjectResponse resp = getApi().getStaticLargeObjectApi(regionId, containerName).delete(name);
assertThat(resp.status()).isEqualTo("200 OK");
assertThat(resp.deleted()).isEqualTo(3);
assertThat(resp.notFound()).isZero();
assertThat(resp.errors()).isEmpty();
assertEquals(getApi().getContainerApi(regionId).get(containerName).getObjectCount(), Long.valueOf(0));
assertDelete(regionId, defaultContainerName, defaultName);
}
}
@Test
public void testReplaceManifestUnicode() throws Exception {
for (String regionId : regions) {
assertReplaceManifest(regionId, unicodeContainerName, unicodeName);
}
}
@Test(dependsOnMethods = "testReplaceManifestUnicode")
public void testDeleteUnicode() throws Exception {
for (String regionId : regions) {
assertDelete(regionId, unicodeContainerName, unicodeName);
}
}
public void testDeleteSinglePartObjectWithMultiPartDelete() throws Exception {
String objectName = "testDeleteSinglePartObjectWithMultiPartDelete";
for (String regionId : regions) {
getApi().getObjectApi(regionId, containerName).put(objectName, newByteSourcePayload(ByteSource.wrap("swifty".getBytes())));
DeleteStaticLargeObjectResponse resp = getApi().getStaticLargeObjectApi(regionId, containerName).delete(objectName);
getApi().getObjectApi(regionId, defaultContainerName).put(objectName, newByteSourcePayload(ByteSource.wrap("swifty".getBytes())));
DeleteStaticLargeObjectResponse resp = getApi().getStaticLargeObjectApi(regionId, defaultContainerName).delete(objectName);
assertThat(resp.status()).isEqualTo("400 Bad Request");
assertThat(resp.deleted()).isZero();
assertThat(resp.notFound()).isZero();
assertThat(resp.errors()).hasSize(1);
getApi().getObjectApi(regionId, containerName).delete(objectName);
getApi().getObjectApi(regionId, defaultContainerName).delete(objectName);
}
}
protected void assertMegabyteAndETagMatches(String regionId, String name, String etag1s) {
protected void assertReplaceManifest(String regionId, String containerName, String name) {
ObjectApi objectApi = getApi().getObjectApi(regionId, containerName);
String etag1s = objectApi.put(name + "/1", newByteSourcePayload(ByteSource.wrap(megOf1s)));
awaitConsistency();
assertMegabyteAndETagMatches(regionId, containerName, name + "/1", etag1s);
String etag2s = objectApi.put(name + "/2", newByteSourcePayload(ByteSource.wrap(megOf2s)));
awaitConsistency();
assertMegabyteAndETagMatches(regionId, containerName, name + "/2", etag2s);
List<Segment> segments = ImmutableList.<Segment> builder()
.add(Segment.builder()
.path(format("%s/%s/1", containerName, name)).etag(etag1s).sizeBytes(1024 * 1024)
.build())
.add(Segment.builder()
.path(format("%s/%s/2", containerName, name)).etag(etag2s).sizeBytes(1024 * 1024)
.build())
.build();
awaitConsistency();
String etagOfEtags = getApi().getStaticLargeObjectApi(regionId, containerName).replaceManifest(
name, segments, ImmutableMap.of("myfoo", "Bar"));
assertNotNull(etagOfEtags);
awaitConsistency();
SwiftObject bigObject = getApi().getObjectApi(regionId, containerName).get(name);
assertEquals(bigObject.getETag(), etagOfEtags);
assertEquals(bigObject.getPayload().getContentMetadata().getContentLength(), Long.valueOf(2 * 1024 * 1024));
assertEquals(bigObject.getMetadata(), ImmutableMap.of("myfoo", "Bar"));
// segments are visible
assertEquals(getApi().getContainerApi(regionId).get(containerName).getObjectCount(), Long.valueOf(3));
}
protected void assertDelete(String regionId, String containerName, String name) {
DeleteStaticLargeObjectResponse resp = getApi().getStaticLargeObjectApi(regionId, containerName).delete(name);
assertThat(resp.status()).isEqualTo("200 OK");
assertThat(resp.deleted()).isEqualTo(3);
assertThat(resp.notFound()).isZero();
assertThat(resp.errors()).isEmpty();
assertEquals(getApi().getContainerApi(regionId).get(containerName).getObjectCount(), Long.valueOf(0));
}
protected void assertMegabyteAndETagMatches(String regionId, String containerName, String name, String etag1s) {
SwiftObject object1s = getApi().getObjectApi(regionId, containerName).get(name);
assertEquals(object1s.getETag(), etag1s);
assertEquals(object1s.getPayload().getContentMetadata().getContentLength(), Long.valueOf(1024 * 1024));
@ -132,9 +156,14 @@ public class StaticLargeObjectApiLiveTest extends BaseSwiftApiLiveTest {
public void setup() {
super.setup();
for (String regionId : regions) {
boolean created = getApi().getContainerApi(regionId).create(containerName);
boolean created = getApi().getContainerApi(regionId).create(defaultContainerName);
if (!created) {
deleteAllObjectsInContainer(regionId, containerName);
deleteAllObjectsInContainer(regionId, defaultContainerName);
}
created = getApi().getContainerApi(regionId).create(unicodeContainerName);
if (!created) {
deleteAllObjectsInContainer(regionId, unicodeContainerName);
}
}
@ -148,8 +177,10 @@ public class StaticLargeObjectApiLiveTest extends BaseSwiftApiLiveTest {
@AfterClass(groups = "live")
public void tearDown() {
for (String regionId : regions) {
deleteAllObjectsInContainer(regionId, containerName);
getApi().getContainerApi(regionId).deleteIfEmpty(containerName);
deleteAllObjectsInContainer(regionId, defaultContainerName);
getApi().getContainerApi(regionId).deleteIfEmpty(defaultContainerName);
deleteAllObjectsInContainer(regionId, unicodeContainerName);
getApi().getContainerApi(regionId).deleteIfEmpty(unicodeContainerName);
}
}
}

View File

@ -19,9 +19,11 @@ package org.jclouds.openstack.swift.v1.features;
import static org.assertj.core.api.Assertions.assertThat;
import static org.jclouds.openstack.swift.v1.reference.SwiftHeaders.OBJECT_METADATA_PREFIX;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;
import java.util.List;
import com.google.common.base.Charsets;
import org.jclouds.openstack.swift.v1.SwiftApi;
import org.jclouds.openstack.swift.v1.domain.DeleteStaticLargeObjectResponse;
import org.jclouds.openstack.swift.v1.domain.Segment;
@ -74,6 +76,52 @@ public class StaticLargeObjectApiMockTest extends BaseOpenStackMockTest<SwiftApi
}
}
public void testReplaceManifestUnicodeUTF8() throws Exception {
MockWebServer server = mockOpenStackServer();
server.enqueue(addCommonHeaders(new MockResponse().setBody(stringFromResource("/access.json"))));
server.enqueue(addCommonHeaders(new MockResponse().addHeader(HttpHeaders.ETAG, "\"abcd\"")));
try {
SwiftApi api = api(server.getUrl("/").toString(), "openstack-swift");
assertEquals(
api.getStaticLargeObjectApi("DFW", "myContainer").replaceManifest(
"unic₪de",
ImmutableList
.<Segment> builder()
.add(Segment.builder().path("/mycontainer/unic₪de/slo/1").etag("0228c7926b8b642dfb29554cd1f00963")
.sizeBytes(1468006).build())
.add(Segment.builder().path("/mycontainer/unic₪de/slo/2")
.etag("5bfc9ea51a00b790717eeb934fb77b9b").sizeBytes(1572864).build())
.add(Segment.builder().path("/mycontainer/unic₪de/slo/3")
.etag("b9c3da507d2557c1ddc51f27c54bae51").sizeBytes(256).build()).build(),
ImmutableMap.of("MyFoo", "Bar")), "abcd");
assertEquals(server.getRequestCount(), 2);
assertAuthentication(server);
RecordedRequest replaceRequest = server.takeRequest();
assertRequest(replaceRequest, "PUT", "/v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/unic%E2%82%AAde?multipart-manifest=put");
assertEquals(replaceRequest.getHeader(OBJECT_METADATA_PREFIX + "myfoo"), "Bar");
String expectedManifest =
"[{\"path\":\"/mycontainer/unic₪de/slo/1\",\"etag\":\"0228c7926b8b642dfb29554cd1f00963\",\"size_bytes\":1468006}," +
"{\"path\":\"/mycontainer/unic₪de/slo/2\",\"etag\":\"5bfc9ea51a00b790717eeb934fb77b9b\",\"size_bytes\":1572864}," +
"{\"path\":\"/mycontainer/unic₪de/slo/3\",\"etag\":\"b9c3da507d2557c1ddc51f27c54bae51\",\"size_bytes\":256}]";
long characterLength = expectedManifest.length();
long byteLength = expectedManifest.getBytes(Charsets.UTF_8).length;
assertNotEquals(characterLength, byteLength);
assertEquals(replaceRequest.getHeader("content-length"), Long.toString(byteLength));
assertEquals(
new String(replaceRequest.getBody()),
expectedManifest);
} finally {
server.shutdown();
}
}
public void testReplaceManifestWithHeaders() throws Exception {
MockWebServer server = mockOpenStackServer();
server.enqueue(addCommonHeaders(new MockResponse().setBody(stringFromResource("/access.json"))));