From 3533ed96782ac7b2afc17644a41513968e26a157 Mon Sep 17 00:00:00 2001 From: "arnett, stu" Date: Fri, 8 Jul 2016 15:39:57 -0500 Subject: [PATCH] v2.2.2 --- 3rd-party-licenses/LICENSE-slf4j-api.txt | 27 +++ 3rd-party-licenses/LICENSE-slf4j-log4j12.txt | 27 +++ build.gradle | 11 +- .../com/emc/object/AbstractJerseyClient.java | 14 +- .../java/com/emc/object/ObjectConfig.java | 13 +- .../java/com/emc/object/ObjectRequest.java | 2 +- .../emc/object/s3/LargeFileDownloader.java | 11 +- .../com/emc/object/s3/LargeFileUploader.java | 21 +- .../java/com/emc/object/s3/S3AuthUtil.java | 14 +- src/main/java/com/emc/object/s3/S3Client.java | 15 +- src/main/java/com/emc/object/s3/S3Config.java | 2 +- .../com/emc/object/s3/S3ObjectMetadata.java | 30 +++ .../java/com/emc/object/s3/bean/PingItem.java | 12 +- .../com/emc/object/s3/bean/PingResponse.java | 18 +- .../com/emc/object/s3/bean/QueryMetadata.java | 67 +++++- .../com/emc/object/s3/bean/QueryObject.java | 21 ++ .../emc/object/s3/jersey/BucketFilter.java | 11 +- .../com/emc/object/s3/jersey/CodecFilter.java | 63 +++++- .../com/emc/object/s3/jersey/ErrorFilter.java | 15 +- .../object/s3/jersey/GeoPinningFilter.java | 15 +- .../emc/object/s3/jersey/NamespaceFilter.java | 11 +- .../com/emc/object/s3/jersey/RetryFilter.java | 18 +- .../object/s3/jersey/S3EncryptionClient.java | 11 +- .../emc/object/s3/jersey/S3JerseyClient.java | 13 +- .../object/s3/request/GetObjectRequest.java | 24 ++ .../object/s3/request/PutObjectRequest.java | 109 +++++++++- .../java/com/emc/object/util/RestUtil.java | 8 +- .../s3/S3EncryptionClientBasicTest.java | 15 ++ .../com/emc/object/s3/S3JerseyClientTest.java | 205 +++++++++++++++++- .../emc/object/s3/S3MetadataSearchTest.java | 7 + .../object/s3/bean/QueryObjectResultTest.java | 130 +++++++++++ 31 files changed, 842 insertions(+), 118 deletions(-) create mode 100644 3rd-party-licenses/LICENSE-slf4j-api.txt create mode 100644 3rd-party-licenses/LICENSE-slf4j-log4j12.txt create mode 100644 src/test/java/com/emc/object/s3/bean/QueryObjectResultTest.java diff --git a/3rd-party-licenses/LICENSE-slf4j-api.txt b/3rd-party-licenses/LICENSE-slf4j-api.txt new file mode 100644 index 00000000..23cc1137 --- /dev/null +++ b/3rd-party-licenses/LICENSE-slf4j-api.txt @@ -0,0 +1,27 @@ +Library: slf4j-api +Copyright 2004-2013 QOS.ch +License: MIT +Full License Text: +----------------------------------------------------------------------------------------------------------------------- + +Copyright (c) 2004-2013 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/3rd-party-licenses/LICENSE-slf4j-log4j12.txt b/3rd-party-licenses/LICENSE-slf4j-log4j12.txt new file mode 100644 index 00000000..0f2937f3 --- /dev/null +++ b/3rd-party-licenses/LICENSE-slf4j-log4j12.txt @@ -0,0 +1,27 @@ +Library: slf4j-log4j12 +Copyright 2004-2013 QOS.ch +License: MIT +Full License Text: +----------------------------------------------------------------------------------------------------------------------- + +Copyright (c) 2004-2013 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3268c00d..40d91452 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ description = 'EMC Object Client for Java - provides REST access to object data ext.githubProjectName = 'ecs-object-client-java' buildscript { - ext.commonBuildVersion = '1.4.1' + ext.commonBuildVersion = '1.5' ext.commonBuildDir = "https://raw.githubusercontent.com/EMCECS/ecs-common-build/v$commonBuildVersion" apply from: "$commonBuildDir/ecs-publish.buildscript.gradle", to: buildscript } @@ -39,8 +39,11 @@ allprojects { } dependencies { - compile 'com.emc.ecs:smart-client:2.0.7', - 'com.emc.ecs:object-transform:1.0.2', - 'org.jdom:jdom2:2.0.6' + compile 'com.emc.ecs:smart-client:2.1.0', + 'com.emc.ecs:object-transform:1.1.0', + 'commons-codec:commons-codec:1.10', + 'org.jdom:jdom2:2.0.6', + 'org.slf4j:slf4j-api:1.7.5' + runtime 'org.slf4j:slf4j-log4j12:1.7.5' testCompile 'junit:junit:4.12' } diff --git a/src/main/java/com/emc/object/AbstractJerseyClient.java b/src/main/java/com/emc/object/AbstractJerseyClient.java index 7aa650f1..8b617954 100644 --- a/src/main/java/com/emc/object/AbstractJerseyClient.java +++ b/src/main/java/com/emc/object/AbstractJerseyClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -33,14 +33,16 @@ import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.client.apache4.config.ApacheHttpClient4Config; -import org.apache.log4j.LogMF; -import org.apache.log4j.Logger; import java.net.URI; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public abstract class AbstractJerseyClient { - private static final Logger l4j = Logger.getLogger(AbstractJerseyClient.class); + + private static final Logger log = LoggerFactory.getLogger(AbstractJerseyClient.class); protected ObjectConfig objectConfig; @@ -69,13 +71,13 @@ protected ClientResponse executeRequest(Client client, ObjectRequest request) { // if content-length is set (perhaps by user), force jersey to use it if (entityRequest.getContentLength() != null) { - LogMF.debug(l4j, "enabling content-length override ({0})", entityRequest.getContentLength()); + log.debug("enabling content-length override ({})", entityRequest.getContentLength().toString()); SizeOverrideWriter.setEntitySize(entityRequest.getContentLength()); // otherwise chunked encoding will be used. if the request does not support it, try to ensure // that the entity is buffered (will set content length from buffered write) } else if (!entityRequest.isChunkable()) { - l4j.debug("no content-length and request is not chunkable, attempting to enable buffering"); + log.debug("no content-length and request is not chunkable, attempting to enable buffering"); request.property(ApacheHttpClient4Config.PROPERTY_ENABLE_BUFFERING, Boolean.TRUE); request.property(ClientConfig.PROPERTY_CHUNKED_ENCODING_SIZE, null); } diff --git a/src/main/java/com/emc/object/ObjectConfig.java b/src/main/java/com/emc/object/ObjectConfig.java index bb6a147e..8a30f1c2 100644 --- a/src/main/java/com/emc/object/ObjectConfig.java +++ b/src/main/java/com/emc/object/ObjectConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -30,14 +30,17 @@ import com.emc.rest.smart.Host; import com.emc.rest.smart.SmartConfig; import com.emc.rest.smart.ecs.Vdc; -import org.apache.log4j.Logger; import java.net.URI; import java.net.URISyntaxException; import java.util.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public abstract class ObjectConfig> { - private static final Logger l4j = Logger.getLogger(ObjectConfig.class); + + private static final Logger log = LoggerFactory.getLogger(ObjectConfig.class); public static final String PROPERTY_POLL_PROTOCOL = "com.emc.object.pollProtocol"; public static final String PROPERTY_POLL_PORT = "com.emc.object.pollPort"; @@ -142,8 +145,8 @@ public URI resolvePath(String relativePath, String rawQuery) { try { URI uri = RestUtil.buildUri(protocol.toString().toLowerCase(), resolveHost().getName(), port, path, rawQuery, null); - l4j.debug("raw path & query: " + path + "?" + rawQuery); - l4j.debug("resolved URI: " + uri); + log.debug("raw path & query: " + path + "?" + rawQuery); + log.debug("resolved URI: " + uri); return uri; } catch (URISyntaxException e) { diff --git a/src/main/java/com/emc/object/ObjectRequest.java b/src/main/java/com/emc/object/ObjectRequest.java index bc545c7d..e731f121 100644 --- a/src/main/java/com/emc/object/ObjectRequest.java +++ b/src/main/java/com/emc/object/ObjectRequest.java @@ -46,7 +46,7 @@ public class ObjectRequest { * dynamic path properties such as bucket or namespace. Since this is context-relative, also exclude * the base context of the service (i.e. /rest for Atmos). * @param subresource the subresource of the request. This will be the first parameter in the querystring and will - * not have an associated value (i.e. "acl" => ?acl). + * not have an associated value (i.e. "acl" => ?acl). */ public ObjectRequest(Method method, String path, String subresource) { this.method = method; diff --git a/src/main/java/com/emc/object/s3/LargeFileDownloader.java b/src/main/java/com/emc/object/s3/LargeFileDownloader.java index f65adc51..edcf6eac 100644 --- a/src/main/java/com/emc/object/s3/LargeFileDownloader.java +++ b/src/main/java/com/emc/object/s3/LargeFileDownloader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -28,7 +28,6 @@ import com.emc.object.Range; import com.emc.object.s3.request.GetObjectRequest; -import org.apache.log4j.Logger; import java.io.File; import java.io.RandomAccessFile; @@ -41,12 +40,16 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Convenience class to facilitate multi-threaded download for large objects. This class will split the object * and download it in parts, transferring several parts simultaneously to maximize efficiency. */ public class LargeFileDownloader implements Runnable { - public static final Logger l4j = Logger.getLogger(LargeFileDownloader.class); + + private static final Logger log = LoggerFactory.getLogger(LargeFileDownloader.class); public static final int MIN_PART_SIZE = 2 * 1024 * 1024; // 2MB public static final int DEFAULT_PART_SIZE = 4 * 1024 * 1024; // 4MB @@ -79,7 +82,7 @@ public void run() { throw new IllegalArgumentException("cannot write to file: " + file.getPath()); if (partSize < MIN_PART_SIZE) { - l4j.warn(String.format("%,dk is below the minimum part size (%,dk). the minimum will be used instead", + log.warn(String.format("%,dk is below the minimum part size (%,dk). the minimum will be used instead", partSize / 1024, MIN_PART_SIZE / 1024)); partSize = MIN_PART_SIZE; } diff --git a/src/main/java/com/emc/object/s3/LargeFileUploader.java b/src/main/java/com/emc/object/s3/LargeFileUploader.java index 64af590d..4f2c447b 100755 --- a/src/main/java/com/emc/object/s3/LargeFileUploader.java +++ b/src/main/java/com/emc/object/s3/LargeFileUploader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -36,7 +36,6 @@ import com.emc.object.util.ProgressInputStream; import com.emc.object.util.ProgressListener; import com.emc.rest.util.SizedInputStream; -import org.apache.log4j.Logger; import java.io.File; import java.io.FileInputStream; @@ -51,12 +50,16 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Convenience class to facilitate multipart upload for large files. This class will split the file * and upload it in parts, transferring several parts simultaneously to maximize efficiency. */ public class LargeFileUploader implements Runnable { - private static final Logger l4j = Logger.getLogger(LargeFileUploader.class); + + private static final Logger log = LoggerFactory.getLogger(LargeFileUploader.class); public static final int DEFAULT_THREADS = 8; @@ -146,7 +149,7 @@ public void doMultipartUpload() { try { s3Client.abortMultipartUpload(new AbortMultipartUploadRequest(bucket, key, uploadId)); } catch (Throwable t) { - l4j.warn("could not abort upload after failure", t); + log.warn("could not abort upload after failure", t); } if (e instanceof RuntimeException) throw (RuntimeException) e; throw new RuntimeException("error during upload", e); @@ -159,7 +162,7 @@ public void doMultipartUpload() { try { stream.close(); } catch (Throwable t) { - l4j.warn("could not close stream", t); + log.warn("could not close stream", t); } } } @@ -197,7 +200,7 @@ public void doByteRangeUpload() { try { s3Client.deleteObject(bucket, key); } catch (Throwable t) { - l4j.warn("could not delete object after failure", t); + log.warn("could not delete object after failure", t); } if (e instanceof RuntimeException) throw (RuntimeException) e; throw new RuntimeException("error during upload", e); @@ -210,7 +213,7 @@ public void doByteRangeUpload() { try { stream.close(); } catch (Throwable t) { - l4j.warn("could not close stream", t); + log.warn("could not close stream", t); } } } @@ -241,11 +244,11 @@ protected void configure() { if (objectMetadata != null) objectMetadata.setContentLength(null); long minPartSize = Math.max(MIN_PART_SIZE, fullSize / MAX_PARTS + 1); - l4j.debug(String.format("minimum part size calculated as %,dk", minPartSize / 1024)); + log.debug(String.format("minimum part size calculated as %,dk", minPartSize / 1024)); if (partSize == null) partSize = minPartSize; if (partSize < minPartSize) { - l4j.warn(String.format("%,dk is below the minimum part size (%,dk). the minimum will be used instead", + log.warn(String.format("%,dk is below the minimum part size (%,dk). the minimum will be used instead", partSize / 1024, minPartSize / 1024)); partSize = minPartSize; } diff --git a/src/main/java/com/emc/object/s3/S3AuthUtil.java b/src/main/java/com/emc/object/s3/S3AuthUtil.java index 21f160a6..b7dab45a 100644 --- a/src/main/java/com/emc/object/s3/S3AuthUtil.java +++ b/src/main/java/com/emc/object/s3/S3AuthUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -31,7 +31,8 @@ import com.emc.object.s3.request.PresignedUrlRequest; import com.emc.object.util.RestUtil; import org.apache.commons.codec.binary.Base64; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -44,7 +45,8 @@ import java.util.*; public final class S3AuthUtil { - private static final Logger l4j = Logger.getLogger(S3AuthUtil.class); + + private static final Logger log = LoggerFactory.getLogger(S3AuthUtil.class); public static SortedSet SIGNED_PARAMETERS; @@ -86,7 +88,7 @@ public static URL generatePresignedUrl(PresignedUrlRequest request, S3Config s3C resource = "/" + namespace + resource; // prepend to resource path for signing } else { // issue warning if namespace is specified and vhost is disabled because we can't put the namespace in the URL - l4j.warn("vHost namespace is disabled, so there is no way to specify a namespace in a pre-signed URL"); + log.warn("vHost namespace is disabled, so there is no way to specify a namespace in a pre-signed URL"); } } @@ -170,7 +172,7 @@ public static String getStringToSign(String method, String resource, Mapkey in bucket bucketName and converts it to objectType, - * provided the conversion is supported by the implementation + * provided the conversion is supported by the implementation. + * Note: this method will return null for 304 and 412 responses (failed preconditions) */ T readObject(String bucketName, String key, Class objectType); /** * Reads version versionId of object key in bucket bucketName and converts - * it to objectType, provided the conversion is supported by the implementation + * it to objectType, provided the conversion is supported by the implementation. + * Note: this method will return null for 304 and 412 responses (failed preconditions) */ T readObject(String bucketName, String key, String versionId, Class objectType); /** - * Reads range bytes of object key in bucket bucketName as a stream + * Reads range bytes of object key in bucket bucketName as a stream. + * Note: this method will return null for 304 and 412 responses (failed preconditions) */ InputStream readObjectStream(String bucketName, String key, Range range); /** * Gets object key in bucket bucketName. Object details as well as the data stream - * (obtained from {@link GetObjectResult#getObject()} are contained in the {@link GetObjectResult} instance + * (obtained from {@link GetObjectResult#getObject()} are contained in the {@link GetObjectResult} instance. + * Note: this method will return null for 304 and 412 responses (failed preconditions) */ GetObjectResult getObject(String bucketName, String key); /** * Gets an object using the parameters specified in request. Object details as well as the translated - * data (converted to objectType) are contained in the {@link GetObjectResult} instance + * data (converted to objectType) are contained in the {@link GetObjectResult} instance. + * Note: this method will return null for 304 and 412 responses (failed preconditions) */ GetObjectResult getObject(GetObjectRequest request, Class objectType); diff --git a/src/main/java/com/emc/object/s3/S3Config.java b/src/main/java/com/emc/object/s3/S3Config.java index 5183b097..e1c9f060 100644 --- a/src/main/java/com/emc/object/s3/S3Config.java +++ b/src/main/java/com/emc/object/s3/S3Config.java @@ -206,7 +206,7 @@ public float getFaultInjectionRate() { } /** - * Sets the fault injection rate. Enables fault injection when this number is > 0. The rate is a ratio expressed as + * Sets the fault injection rate. Enables fault injection when this number is > 0. The rate is a ratio expressed as * a decimal between 0 and 1. This is the rate at which faults (HTTP 500 errors) should randomly be injected into * the response. When faults are injected, the real request is never sent over the wire. Fault injection is disabled * by default. diff --git a/src/main/java/com/emc/object/s3/S3ObjectMetadata.java b/src/main/java/com/emc/object/s3/S3ObjectMetadata.java index 73063198..d92c63d4 100644 --- a/src/main/java/com/emc/object/s3/S3ObjectMetadata.java +++ b/src/main/java/com/emc/object/s3/S3ObjectMetadata.java @@ -44,6 +44,8 @@ public class S3ObjectMetadata { private String contentMd5; private String contentType; private String eTag; + private Long retentionPeriod; + private String retentionPolicy; private Date expirationDate; private String expirationRuleId; private Date httpExpires; @@ -71,6 +73,10 @@ public static S3ObjectMetadata fromHeaders(Map> headers) { objectMetadata.lastModified = RestUtil.headerParse(RestUtil.getFirstAsString(headers, RestUtil.HEADER_LAST_MODIFIED)); objectMetadata.versionId = RestUtil.getFirstAsString(headers, S3Constants.AMZ_VERSION_ID); + if (RestUtil.getFirstAsString(headers, RestUtil.EMC_RETENTION_PERIOD) != null) { + objectMetadata.retentionPeriod = Long.parseLong(RestUtil.getFirstAsString(headers, RestUtil.EMC_RETENTION_PERIOD)); + } + objectMetadata.retentionPolicy = RestUtil.getFirstAsString(headers, RestUtil.EMC_RETENTION_POLICY); objectMetadata.expirationDate = getExpirationDate(headers); objectMetadata.expirationRuleId = getExpirationRuleId(headers); objectMetadata.userMetadata = getUserMetadata(headers); @@ -129,6 +135,14 @@ public Map> toHeaders() { RestUtil.putSingle(headers, RestUtil.HEADER_CONTENT_MD5, contentMd5); RestUtil.putSingle(headers, RestUtil.HEADER_CONTENT_TYPE, contentType); RestUtil.putSingle(headers, RestUtil.HEADER_EXPIRES, RestUtil.headerFormat(httpExpires)); + RestUtil.putSingle(headers, RestUtil.EMC_RETENTION_PERIOD, retentionPeriod); + RestUtil.putSingle(headers, RestUtil.EMC_RETENTION_POLICY, retentionPolicy); + headers.putAll(getUmdHeaders(userMetadata)); + return headers; + } + + public static Map> getUmdHeaders(Map userMetadata) { + Map> headers = new HashMap>(); for (String name : userMetadata.keySet()) { RestUtil.putSingle(headers, getHeaderName(name), userMetadata.get(name)); } @@ -195,6 +209,22 @@ public void setETag(String eTag) { this.eTag = eTag; } + public Long getRetentionPeriod() { + return retentionPeriod; + } + + public void setRetentionPeriod(Long retentionPeriod) { + this.retentionPeriod = retentionPeriod; + } + + public String getRetentionPolicy() { + return retentionPolicy; + } + + public void setRetentionPolicy(String retentionPolicy) { + this.retentionPolicy = retentionPolicy; + } + public Date getExpirationDate() { return expirationDate; } diff --git a/src/main/java/com/emc/object/s3/bean/PingItem.java b/src/main/java/com/emc/object/s3/bean/PingItem.java index c6d47c1c..b83461f5 100644 --- a/src/main/java/com/emc/object/s3/bean/PingItem.java +++ b/src/main/java/com/emc/object/s3/bean/PingItem.java @@ -35,6 +35,7 @@ public class PingItem { String name; Status status; String text; + String value; @XmlElement(name = "Name") public String getName() { @@ -63,8 +64,17 @@ public void setText(String text) { this.text = text; } + @XmlElement(name = "Value") + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + @XmlEnum - public static enum Status { + public enum Status { OFF, UNKNOWN, ON } } diff --git a/src/main/java/com/emc/object/s3/bean/PingResponse.java b/src/main/java/com/emc/object/s3/bean/PingResponse.java index 96b2a66d..546ff306 100644 --- a/src/main/java/com/emc/object/s3/bean/PingResponse.java +++ b/src/main/java/com/emc/object/s3/bean/PingResponse.java @@ -36,29 +36,31 @@ @XmlRootElement(name = "PingList") public class PingResponse { - Map pingItemMap; + private List pingItems = new ArrayList(); @XmlElement(name = "PingItem") public List getPingItems() { - if (pingItemMap == null) return null; - return new ArrayList(pingItemMap.values()); + return pingItems; } public void setPingItems(List pingItems) { + this.pingItems = pingItems; + } + + @XmlTransient + public Map getPingItemMap() { + Map pingItemMap = null; if (pingItems != null) { pingItemMap = new HashMap(); for (PingItem item : pingItems) { pingItemMap.put(item.getName(), item); } } - } - - @XmlTransient - public Map getPingItemMap() { return pingItemMap; } public void setPingItemMap(Map pingItemMap) { - this.pingItemMap = pingItemMap; + if (pingItemMap == null) this.pingItems = null; + else this.pingItems = new ArrayList(pingItemMap.values()); } } diff --git a/src/main/java/com/emc/object/s3/bean/QueryMetadata.java b/src/main/java/com/emc/object/s3/bean/QueryMetadata.java index f3687fe4..5b5dc391 100644 --- a/src/main/java/com/emc/object/s3/bean/QueryMetadata.java +++ b/src/main/java/com/emc/object/s3/bean/QueryMetadata.java @@ -28,13 +28,14 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlType; -import java.util.HashMap; -import java.util.Map; +import javax.xml.bind.annotation.adapters.XmlAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.*; -@XmlType(namespace = "") +@XmlType(propOrder = {"type", "mdMap"}, namespace = "") public class QueryMetadata { private QueryMetadataType type; - private Map mdMap = new HashMap(); + private Map mdMap = new TreeMap(); @XmlElement(name = "type") public QueryMetadataType getType() { @@ -45,6 +46,7 @@ public void setType(QueryMetadataType type) { this.type = type; } + @XmlJavaTypeAdapter(MapAdapter.class) @XmlElement(name = "mdMap") public Map getMdMap() { return mdMap; @@ -53,4 +55,61 @@ public Map getMdMap() { public void setMdMap(Map mdMap) { this.mdMap = mdMap; } + + public static class MapAdapter extends XmlAdapter> { + @Override + public Map unmarshal(FlatMap v) throws Exception { + Map map = new TreeMap(); + for (Entry entry : v.entry) { + map.put(entry.key, entry.value); + } + return map; + } + + @Override + public FlatMap marshal(Map v) throws Exception { + FlatMap flatMap = new FlatMap(); + for (String key : v.keySet()) { + flatMap.entry.add(new Entry(key, v.get(key))); + } + return flatMap; + } + } + + @XmlType(namespace = "") + public static class FlatMap { + public List entry = new ArrayList(); + } + + @XmlType(namespace = "") + public static class Entry { + public String key; + public String value; + + public Entry() { + } + + public Entry(String key, String value) { + this.key = key; + this.value = value; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + QueryMetadata metadata = (QueryMetadata) o; + + if (type != metadata.type) return false; + return mdMap != null ? mdMap.equals(metadata.mdMap) : metadata.mdMap == null; + } + + @Override + public int hashCode() { + int result = type != null ? type.hashCode() : 0; + result = 31 * result + (mdMap != null ? mdMap.hashCode() : 0); + return result; + } } diff --git a/src/main/java/com/emc/object/s3/bean/QueryObject.java b/src/main/java/com/emc/object/s3/bean/QueryObject.java index d5fd4215..564ea2e8 100644 --- a/src/main/java/com/emc/object/s3/bean/QueryObject.java +++ b/src/main/java/com/emc/object/s3/bean/QueryObject.java @@ -72,4 +72,25 @@ public void setQueryMds(List queryMds) { this.queryMds = queryMds; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + QueryObject object = (QueryObject) o; + + if (objectName != null ? !objectName.equals(object.objectName) : object.objectName != null) return false; + if (objectId != null ? !objectId.equals(object.objectId) : object.objectId != null) return false; + if (versionId != null ? !versionId.equals(object.versionId) : object.versionId != null) return false; + return queryMds != null ? queryMds.equals(object.queryMds) : object.queryMds == null; + } + + @Override + public int hashCode() { + int result = objectName != null ? objectName.hashCode() : 0; + result = 31 * result + (objectId != null ? objectId.hashCode() : 0); + result = 31 * result + (versionId != null ? versionId.hashCode() : 0); + result = 31 * result + (queryMds != null ? queryMds.hashCode() : 0); + return result; + } } diff --git a/src/main/java/com/emc/object/s3/jersey/BucketFilter.java b/src/main/java/com/emc/object/s3/jersey/BucketFilter.java index 1bde2ffa..bb9720c1 100644 --- a/src/main/java/com/emc/object/s3/jersey/BucketFilter.java +++ b/src/main/java/com/emc/object/s3/jersey/BucketFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -33,13 +33,16 @@ import com.sun.jersey.api.client.ClientRequest; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.filter.ClientFilter; -import org.apache.log4j.Logger; import java.net.URI; import java.net.URISyntaxException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class BucketFilter extends ClientFilter { - private static final Logger l4j = Logger.getLogger(BucketFilter.class); + + private static final Logger log = LoggerFactory.getLogger(BucketFilter.class); public static URI insertBucket(URI uri, String bucketName, boolean useVHost) { try { @@ -52,7 +55,7 @@ public static URI insertBucket(URI uri, String bucketName, boolean useVHost) { uri = RestUtil.replacePath(uri, resource); } - l4j.debug("URI including bucket: " + uri); + log.debug("URI including bucket: " + uri); return uri; } catch (URISyntaxException e) { throw new RuntimeException(String.format("bucket name \"%s\" generated an invalid URI", bucketName), e); diff --git a/src/main/java/com/emc/object/s3/jersey/CodecFilter.java b/src/main/java/com/emc/object/s3/jersey/CodecFilter.java index 8fc7f30a..c54b337d 100644 --- a/src/main/java/com/emc/object/s3/jersey/CodecFilter.java +++ b/src/main/java/com/emc/object/s3/jersey/CodecFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -32,10 +32,13 @@ import com.emc.rest.smart.SizeOverrideWriter; import com.sun.jersey.api.client.*; import com.sun.jersey.api.client.filter.ClientFilter; -import org.apache.log4j.LogMF; -import org.apache.log4j.Logger; import javax.ws.rs.core.MultivaluedMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.HashSet; @@ -43,7 +46,8 @@ import java.util.Set; public class CodecFilter extends ClientFilter { - private static final Logger l4j = Logger.getLogger(CodecFilter.class); + + private static final Logger log = LoggerFactory.getLogger(CodecFilter.class); private CodecChain encodeChain; private Map codecProperties; @@ -64,15 +68,24 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio Long originalSize = SizeOverrideWriter.getEntitySize(); if (encodeChain.isSizePredictable() && originalSize != null) { long encodedSize = encodeChain.getEncodedSize(originalSize); - LogMF.debug(l4j, "updating content-length for encoded data (original: {0}, encoded: {1})", originalSize, encodedSize); + log.debug("updating content-length for encoded data (original: {}, encoded: {})", originalSize, encodedSize); SizeOverrideWriter.setEntitySize(encodedSize); } else { // we don't know what the size will be; this will turn on chunked encoding in the apache client SizeOverrideWriter.setEntitySize(-1L); } + // we need pre-stream metadata from the encoder, but we don't have the entity output stream, so we'll use + // a "dangling" output stream and connect it in the adapter + // NOTE: we can't alter the headers in the adapt() method because they've already been a) signed and b) sent + DanglingOutputStream danglingStream = new DanglingOutputStream(); + OutputStream encodeStream = encodeChain.getEncodeStream(danglingStream, userMeta); + + // add pre-stream encode metadata + request.getHeaders().putAll(S3ObjectMetadata.getUmdHeaders(userMeta)); + // wrap output stream with encryptor - request.setAdapter(new EncryptAdapter(request.getAdapter(), userMeta)); + request.setAdapter(new EncryptAdapter(request.getAdapter(), danglingStream, encodeStream)); } // execute request @@ -120,16 +133,19 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio // only way to set the output stream private class EncryptAdapter extends AbstractClientRequestAdapter { - Map encMeta; + DanglingOutputStream danglingStream; + OutputStream encodeStream; - EncryptAdapter(ClientRequestAdapter parent, Map encMeta) { + EncryptAdapter(ClientRequestAdapter parent, DanglingOutputStream danglingStream, OutputStream encodeStream) { super(parent); - this.encMeta = encMeta; + this.danglingStream = danglingStream; + this.encodeStream = encodeStream; } @Override public OutputStream adapt(ClientRequest request, OutputStream out) throws IOException { - return getAdapter().adapt(request, encodeChain.getEncodeStream(out, encMeta)); // don't break the chain + danglingStream.setOutputStream(out); // connect the dangling output stream + return getAdapter().adapt(request, encodeStream); // don't break the chain } } @@ -145,4 +161,31 @@ public CodecFilter withCodecProperties(Map codecProperties) { setCodecProperties(codecProperties); return this; } + + private static class DanglingOutputStream extends FilterOutputStream { + private static final OutputStream BOGUS_STREAM = new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new RuntimeException("you didn't connect a dangling output stream!"); + } + }; + + DanglingOutputStream() { + super(BOGUS_STREAM); + } + + void setOutputStream(OutputStream out) { + this.out = out; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + + @Override + public void write(int b) throws IOException { + throw new UnsupportedOperationException("single-byte write called!"); + } + } } diff --git a/src/main/java/com/emc/object/s3/jersey/ErrorFilter.java b/src/main/java/com/emc/object/s3/jersey/ErrorFilter.java index ade9fbc2..a2ac46f7 100755 --- a/src/main/java/com/emc/object/s3/jersey/ErrorFilter.java +++ b/src/main/java/com/emc/object/s3/jersey/ErrorFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -33,11 +33,11 @@ import com.sun.jersey.api.client.ClientRequest; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.filter.ClientFilter; -import org.apache.log4j.LogMF; -import org.apache.log4j.Logger; import org.jdom2.Document; import org.jdom2.Namespace; import org.jdom2.input.SAXBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.ws.rs.core.Response; import java.io.InputStreamReader; @@ -45,7 +45,8 @@ import java.util.Date; public class ErrorFilter extends ClientFilter { - private static final Logger l4j = Logger.getLogger(ErrorFilter.class); + + private static final Logger log = LoggerFactory.getLogger(ErrorFilter.class); public ClientResponse handle(ClientRequest request) throws ClientHandlerException { ClientResponse response = getNext().handle(request); @@ -61,7 +62,7 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio if (clientTime != null && serverTime != null) { long skew = clientTime.getTime() - serverTime.getTime(); if (Math.abs(skew) > 5 * 60 * 1000) { // +/- 5 minutes - l4j.warn("clock skew detected! client is more than 5 minutes off from server (" + skew + "ms)"); + log.warn("clock skew detected! client is more than 5 minutes off from server (" + skew + "ms)"); } } } @@ -110,7 +111,7 @@ public static S3Exception parseErrorResponse(Reader reader, int statusCode) { try { reader.close(); } catch (Throwable t) { - l4j.warn("could not close reader", t); + log.warn("could not close reader", t); } } @@ -131,7 +132,7 @@ public static S3Exception parseErrorResponse(Reader reader, int statusCode) { return new S3Exception("no code or message in error response", statusCode); } - LogMF.debug(l4j, "Error: {0}, message: {1}, requestId: {2}", code, message, requestId); + log.debug("Error: {}, message: {}, requestId: {}", new Object[] { code, message, requestId }); return new S3Exception(message, statusCode, code, requestId); } } diff --git a/src/main/java/com/emc/object/s3/jersey/GeoPinningFilter.java b/src/main/java/com/emc/object/s3/jersey/GeoPinningFilter.java index c9459bc4..f8b8a2d2 100644 --- a/src/main/java/com/emc/object/s3/jersey/GeoPinningFilter.java +++ b/src/main/java/com/emc/object/s3/jersey/GeoPinningFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -35,8 +35,8 @@ import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.filter.ClientFilter; import org.apache.commons.codec.digest.DigestUtils; -import org.apache.log4j.LogMF; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -47,7 +47,8 @@ * the path to extract the object key) */ public class GeoPinningFilter extends ClientFilter { - private static final Logger l4j = Logger.getLogger(GeoPinningFilter.class); + + private static final Logger log = LoggerFactory.getLogger(GeoPinningFilter.class); /** * If this is a bucket request, the bucket is the ID. @@ -84,7 +85,7 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio } if (healthyVdcs.isEmpty()) { - l4j.debug("there are no healthy VDCs; geo-pinning will include all VDCs"); + log.debug("there are no healthy VDCs; geo-pinning will include all VDCs"); healthyVdcs.addAll(objectConfig.getVdcs()); } @@ -95,8 +96,8 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio Integer retries = (Integer) request.getProperties().get(RetryFilter.PROP_RETRY_COUNT); if (retries != null) { int newIndex = (geoPinIndex + retries) % healthyVdcs.size(); - LogMF.info(l4j, "geo-pin read retry #{0}: failing over from primary VDC {1} to VDC {2}", - retries, geoPinIndex, newIndex); + log.info("geo-pin read retry #{}: failing over from primary VDC {} to VDC {}", + new Object[] { retries, geoPinIndex, newIndex }); geoPinIndex = newIndex; } } diff --git a/src/main/java/com/emc/object/s3/jersey/NamespaceFilter.java b/src/main/java/com/emc/object/s3/jersey/NamespaceFilter.java index 70a57b4e..8e8c7986 100644 --- a/src/main/java/com/emc/object/s3/jersey/NamespaceFilter.java +++ b/src/main/java/com/emc/object/s3/jersey/NamespaceFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -32,13 +32,16 @@ import com.sun.jersey.api.client.ClientRequest; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.filter.ClientFilter; -import org.apache.log4j.Logger; import java.net.URI; import java.net.URISyntaxException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class NamespaceFilter extends ClientFilter { - private static final Logger l4j = Logger.getLogger(NamespaceFilter.class); + + private static final Logger log = LoggerFactory.getLogger(NamespaceFilter.class); /** * prepend to hostname (i.e. namespace.s3.company.com) @@ -46,7 +49,7 @@ public class NamespaceFilter extends ClientFilter { public static URI insertNamespace(URI uri, String namespace) { try { String hostname = namespace + "." + uri.getHost(); - l4j.debug(String.format("hostname including namespace: %s", hostname)); + log.debug("hostname including namespace: {}", hostname); return RestUtil.replaceHost(uri, hostname); } catch (URISyntaxException e) { throw new RuntimeException(String.format("namespace \"%s\" generated an invalid URI", namespace), e); diff --git a/src/main/java/com/emc/object/s3/jersey/RetryFilter.java b/src/main/java/com/emc/object/s3/jersey/RetryFilter.java index 2f09ad93..fd98efd6 100644 --- a/src/main/java/com/emc/object/s3/jersey/RetryFilter.java +++ b/src/main/java/com/emc/object/s3/jersey/RetryFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, EMC Corporation. + * Copyright (c) 2015-2016, EMC Corporation. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * @@ -32,14 +32,16 @@ import com.sun.jersey.api.client.ClientRequest; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.filter.ClientFilter; -import org.apache.log4j.LogMF; -import org.apache.log4j.Logger; import java.io.IOException; import java.io.InputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class RetryFilter extends ClientFilter { - private static final Logger l4j = Logger.getLogger(RetryFilter.class); + + private static final Logger log = LoggerFactory.getLogger(RetryFilter.class); public static final String PROP_RETRY_COUNT = "com.emc.object.retryCount"; @@ -85,7 +87,7 @@ public ClientResponse handle(ClientRequest clientRequest) throws ClientHandlerEx if (!entityStream.markSupported()) throw new IOException("stream does not support mark/reset"); entityStream.reset(); } catch (IOException e) { - l4j.warn("could not reset entity stream for retry: " + e); + log.warn("could not reset entity stream for retry: " + e); throw orig; } } @@ -94,14 +96,14 @@ public ClientResponse handle(ClientRequest clientRequest) throws ClientHandlerEx if (s3Config.getInitialRetryDelay() > 0) { int retryDelay = s3Config.getInitialRetryDelay() * (int) Math.pow(2, retryCount - 1); try { - LogMF.debug(l4j, "waiting {0}ms before retry", retryDelay); + log.debug("waiting {}ms before retry", retryDelay); Thread.sleep(retryDelay); } catch (InterruptedException e) { - l4j.warn("interrupted while waiting to retry: " + e.getMessage()); + log.warn("interrupted while waiting to retry: " + e.getMessage()); } } - LogMF.info(l4j, "error received in response [{0}], retrying ({1} of {2})...", t, retryCount, s3Config.getRetryLimit()); + log.info("error received in response [{}], retrying ({} of {})...", new Object[] { t, retryCount, s3Config.getRetryLimit() }); clientRequest.getProperties().put(PROP_RETRY_COUNT, retryCount); } } diff --git a/src/main/java/com/emc/object/s3/jersey/S3EncryptionClient.java b/src/main/java/com/emc/object/s3/jersey/S3EncryptionClient.java index 31d83f00..12111f8f 100644 --- a/src/main/java/com/emc/object/s3/jersey/S3EncryptionClient.java +++ b/src/main/java/com/emc/object/s3/jersey/S3EncryptionClient.java @@ -65,7 +65,7 @@ * -keysize 2048 -dname "CN=My Name, OU=My Division, O=My Company, L=My Location, ST=MA, C=US" * Enter keystore password: changeit * Re-enter new password: changeit - * Enter key password for + * Enter key password for <masterkey> * (RETURN if same as keystore password): * * Inside your application, you can then construct and load a Keystore object, @@ -123,7 +123,7 @@ public S3EncryptionClient(S3Config s3Config, ClientHandler clientHandler, Encryp : new CodecChain(encryptionConfig.getEncryptionSpec()); encodeChain.setProperties(encryptionConfig.getCodecProperties()); - // insert codec filter into chain before the checksum filter + // insert codec filter into chain before the authorization filter // as usual, Jersey makes this quite hard // first, make a list of the filters @@ -131,7 +131,7 @@ public S3EncryptionClient(S3Config s3Config, ClientHandler clientHandler, Encryp ClientHandler handler = client.getHeadHandler(); while (handler instanceof ClientFilter) { ClientFilter filter = (ClientFilter) handler; - if (filter instanceof ChecksumFilter) { + if (filter instanceof AuthorizationFilter) { // insert codec filter before checksum filter filters.add(new CodecFilter(encodeChain).withCodecProperties(encryptionConfig.getCodecProperties())); } @@ -197,13 +197,14 @@ public PutObjectResult putObject(PutObjectRequest request) { request.property(RestUtil.PROPERTY_ENCODE_ENTITY, Boolean.TRUE); // write data - super.putObject(request); + PutObjectResult result = super.putObject(request); // encryption filter will modify userMeta with encryption metadata *after* the object is transferred // we must send a separate metadata update or the object will be unreadable // TODO: should this be atomic? how do we handle rollback? CopyObjectRequest metadataUpdate = new CopyObjectRequest(request.getBucketName(), request.getKey(), - request.getBucketName(), request.getKey()).withAcl(request.getAcl()).withObjectMetadata(request.getObjectMetadata()); + request.getBucketName(), request.getKey()).withAcl(request.getAcl()) + .withObjectMetadata(request.getObjectMetadata()).withIfMatch(result.getETag()); return super.copyObject(metadataUpdate); } diff --git a/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java b/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java index 810d24d6..b75d79a8 100644 --- a/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java +++ b/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java @@ -229,10 +229,10 @@ public void shutdown() { /** * Destroy the client. Any system resources associated with the client * will be cleaned up. - *

+ *

* This method must be called when there are not responses pending otherwise * undefined behavior will occur. - *

+ *

* The client must not be reused after this method is called otherwise * undefined behavior will occur. */ @@ -538,17 +538,20 @@ public CopyObjectResult copyObject(CopyObjectRequest request) { @Override public T readObject(String bucketName, String key, Class objectType) { - return getObject(new GetObjectRequest(bucketName, key), objectType).getObject(); + GetObjectResult result = getObject(new GetObjectRequest(bucketName, key), objectType); + return result == null ? null : result.getObject(); } @Override public T readObject(String bucketName, String key, String versionId, Class objectType) { - return getObject(new GetObjectRequest(bucketName, key).withVersionId(versionId), objectType).getObject(); + GetObjectResult result = getObject(new GetObjectRequest(bucketName, key).withVersionId(versionId), objectType); + return result == null ? null : result.getObject(); } @Override public InputStream readObjectStream(String bucketName, String key, Range range) { - return getObject(new GetObjectRequest(bucketName, key).withRange(range), InputStream.class).getObject(); + GetObjectResult result = getObject(new GetObjectRequest(bucketName, key).withRange(range), InputStream.class); + return result == null ? null : result.getObject(); } @Override diff --git a/src/main/java/com/emc/object/s3/request/GetObjectRequest.java b/src/main/java/com/emc/object/s3/request/GetObjectRequest.java index d1e68a89..bda0a23a 100644 --- a/src/main/java/com/emc/object/s3/request/GetObjectRequest.java +++ b/src/main/java/com/emc/object/s3/request/GetObjectRequest.java @@ -143,6 +143,30 @@ public T withRange(Range range) { return (T) this; } + @SuppressWarnings("unchecked") + public T withIfModifiedSince(Date ifModifiedSince) { + setIfModifiedSince(ifModifiedSince); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T withIfUnmodifiedSince(Date ifUnmodifiedSince) { + setIfUnmodifiedSince(ifUnmodifiedSince); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T withIfMatch(String ifMatch) { + setIfMatch(ifMatch); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T withIfNoneMatch(String ifNoneMatch) { + setIfNoneMatch(ifNoneMatch); + return (T) this; + } + @SuppressWarnings("unchecked") public T headerOverride(ResponseHeaderOverride override, String value) { headerOverrides.put(override, value); diff --git a/src/main/java/com/emc/object/s3/request/PutObjectRequest.java b/src/main/java/com/emc/object/s3/request/PutObjectRequest.java index aa6b57ed..afac6f89 100644 --- a/src/main/java/com/emc/object/s3/request/PutObjectRequest.java +++ b/src/main/java/com/emc/object/s3/request/PutObjectRequest.java @@ -35,6 +35,7 @@ import com.emc.object.s3.bean.CannedAcl; import com.emc.object.util.RestUtil; +import java.util.Date; import java.util.List; import java.util.Map; @@ -42,10 +43,12 @@ public class PutObjectRequest extends S3ObjectRequest implements EntityRequest { private S3ObjectMetadata objectMetadata; private Object object; private Range range; + private Date ifModifiedSince; + private Date ifUnmodifiedSince; + private String ifMatch; + private String ifNoneMatch; private AccessControlList acl; private CannedAcl cannedAcl; - private Long retentionPeriod; - private String retentionPolicy; public PutObjectRequest(String bucketName, String key, Object object) { super(Method.PUT, bucketName, key, null); @@ -66,10 +69,14 @@ public Map> getHeaders() { Map> headers = super.getHeaders(); if (range != null) RestUtil.putSingle(headers, RestUtil.HEADER_RANGE, "bytes=" + range.toString()); if (objectMetadata != null) headers.putAll(objectMetadata.toHeaders()); + if (ifModifiedSince != null) + RestUtil.putSingle(headers, RestUtil.HEADER_IF_MODIFIED_SINCE, RestUtil.headerFormat(ifModifiedSince)); + if (ifUnmodifiedSince != null) + RestUtil.putSingle(headers, RestUtil.HEADER_IF_UNMODIFIED_SINE, RestUtil.headerFormat(ifUnmodifiedSince)); + if (ifMatch != null) RestUtil.putSingle(headers, RestUtil.HEADER_IF_MATCH, ifMatch); + if (ifNoneMatch != null) RestUtil.putSingle(headers, RestUtil.HEADER_IF_NONE_MATCH, ifNoneMatch); if (acl != null) headers.putAll(acl.toHeaders()); if (cannedAcl != null) RestUtil.putSingle(headers, S3Constants.AMZ_ACL, cannedAcl.getHeaderValue()); - if (retentionPeriod != null) RestUtil.putSingle(headers, RestUtil.EMC_RETENTION_PERIOD, retentionPeriod); - if (retentionPolicy != null) RestUtil.putSingle(headers, RestUtil.EMC_RETENTION_POLICY, retentionPolicy); return headers; } @@ -113,6 +120,38 @@ public void setRange(Range range) { this.range = range; } + public Date getIfModifiedSince() { + return ifModifiedSince; + } + + public void setIfModifiedSince(Date ifModifiedSince) { + this.ifModifiedSince = ifModifiedSince; + } + + public Date getIfUnmodifiedSince() { + return ifUnmodifiedSince; + } + + public void setIfUnmodifiedSince(Date ifUnmodifiedSince) { + this.ifUnmodifiedSince = ifUnmodifiedSince; + } + + public String getIfMatch() { + return ifMatch; + } + + public void setIfMatch(String ifMatch) { + this.ifMatch = ifMatch; + } + + public String getIfNoneMatch() { + return ifNoneMatch; + } + + public void setIfNoneMatch(String ifNoneMatch) { + this.ifNoneMatch = ifNoneMatch; + } + public AccessControlList getAcl() { return acl; } @@ -129,28 +168,48 @@ public void setCannedAcl(CannedAcl cannedAcl) { this.cannedAcl = cannedAcl; } + /** + * @deprecated Use the method com.emc.object.s3.S3ObjectMetadata.getRetentionPeriod in preference to this one. + * @return The retention period in seconds. + */ + @Deprecated public Long getRetentionPeriod() { - return retentionPeriod; + return (objectMetadata == null) ? null : objectMetadata.getRetentionPeriod(); } /** * Sets the retention (read-only) period for the object in seconds (after retentionPeriod seconds, * you can modify or delete the object) + * @deprecated Use the method com.emc.object.s3.S3ObjectMetadata.setRetentionPeriod in preference to this one. */ + @Deprecated public void setRetentionPeriod(Long retentionPeriod) { - this.retentionPeriod = retentionPeriod; + if (objectMetadata == null) { + objectMetadata = new S3ObjectMetadata(); + } + objectMetadata.setRetentionPeriod(retentionPeriod); } + /** + * @deprecated Use the method com.emc.object.s3.S3ObjectMetadata.getRetentionPolicy in preference to this one. + * @return The retention policy name. + */ + @Deprecated public String getRetentionPolicy() { - return retentionPolicy; + return (objectMetadata == null) ? null : objectMetadata.getRetentionPolicy(); } /** * Sets the name of the retention policy to apply to the object. Retention policies are defined within each * namespace + * @deprecated Use the method com.emc.object.s3.S3ObjectMetadata.setRetentionPolicy in preference to this one. */ + @Deprecated public void setRetentionPolicy(String retentionPolicy) { - this.retentionPolicy = retentionPolicy; + if (objectMetadata == null) { + objectMetadata = new S3ObjectMetadata(); + } + objectMetadata.setRetentionPolicy(retentionPolicy); } public PutObjectRequest withObjectMetadata(S3ObjectMetadata objectMetadata) { @@ -163,6 +222,26 @@ public PutObjectRequest withRange(Range range) { return this; } + public PutObjectRequest withIfModifiedSince(Date ifModifiedSince) { + setIfModifiedSince(ifModifiedSince); + return this; + } + + public PutObjectRequest withIfUnmodifiedSince(Date ifUnmodifiedSince) { + setIfUnmodifiedSince(ifUnmodifiedSince); + return this; + } + + public PutObjectRequest withIfMatch(String ifMatch) { + setIfMatch(ifMatch); + return this; + } + + public PutObjectRequest withIfNoneMatch(String ifNoneMatch) { + setIfNoneMatch(ifNoneMatch); + return this; + } + public PutObjectRequest withAcl(AccessControlList acl) { setAcl(acl); return this; @@ -173,11 +252,25 @@ public PutObjectRequest withCannedAcl(CannedAcl cannedAcl) { return this; } + /** + * Convenience method. + * @deprecated Use the method com.emc.object.s3.S3ObjectMetadata.setRetentionPeriod in preference to this one. + * @param retentionPeriod + * @return The request. + */ + @Deprecated public PutObjectRequest withRetentionPeriod(long retentionPeriod) { setRetentionPeriod(retentionPeriod); return this; } + /** + * Convenience method. + * @deprecated Use the method com.emc.object.s3.S3ObjectMetadata.setRetentionPolicy in preference to this one. + * @param retentionPolicy + * @return The request. + */ + @Deprecated public PutObjectRequest withRetentionPolicy(String retentionPolicy) { setRetentionPolicy(retentionPolicy); return this; diff --git a/src/main/java/com/emc/object/util/RestUtil.java b/src/main/java/com/emc/object/util/RestUtil.java index 377e2953..6fd01172 100644 --- a/src/main/java/com/emc/object/util/RestUtil.java +++ b/src/main/java/com/emc/object/util/RestUtil.java @@ -264,16 +264,16 @@ public static URI buildUri(String scheme, String host, int port, String path, St /** * Returns the content of this URI as a US-ASCII string. * - *

Note: this starts our customized version of URI's toASCIIString. We differ in only one aspect: we do + *

Note: this starts our customized version of URI's toASCIIString. We differ in only one aspect: we do * NOT normalize Unicode characters. This is because certain Unicode characters may have different compositions * and normalization may change the UTF-8 sequence represented by a character. We must maintain the same UTF-8 - * sequence in and out and therefore we cannot normalize the sequences. + * sequence in and out and therefore we cannot normalize the sequences.

* *

If this URI does not contain any characters in the other * category then an invocation of this method will return the same value as * an invocation of the {@link #toString() toString} method. Otherwise - * this method works as if by invoking that method and then encoding the result.

+ * this method works as if by invoking that method and then + * encoding the result.

* * @return The string form of this URI, encoded as needed * so that it only contains characters in the US-ASCII diff --git a/src/test/java/com/emc/object/s3/S3EncryptionClientBasicTest.java b/src/test/java/com/emc/object/s3/S3EncryptionClientBasicTest.java index 9b0916d0..1afe8e3a 100644 --- a/src/test/java/com/emc/object/s3/S3EncryptionClientBasicTest.java +++ b/src/test/java/com/emc/object/s3/S3EncryptionClientBasicTest.java @@ -451,4 +451,19 @@ public void testLargeFileUploaderProgressListener() throws Exception { @Override public void testCopyObjectWithMeta() throws Exception { } + + @Ignore + @Override + public void testCreateObjectWithStream() throws Exception { + } + + @Ignore + @Override + public void testCreateObjectWithRetentionPeriod() throws Exception { + } + + @Ignore + @Override + public void testCreateObjectWithRetentionPolicy() throws Exception { + } } diff --git a/src/test/java/com/emc/object/s3/S3JerseyClientTest.java b/src/test/java/com/emc/object/s3/S3JerseyClientTest.java index bbb676be..e24d85ee 100644 --- a/src/test/java/com/emc/object/s3/S3JerseyClientTest.java +++ b/src/test/java/com/emc/object/s3/S3JerseyClientTest.java @@ -38,6 +38,7 @@ import com.emc.rest.smart.Host; import com.emc.rest.smart.ecs.Vdc; import com.emc.rest.smart.ecs.VdcHost; +import com.emc.util.RandomInputStream; import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.client.apache4.config.ApacheHttpClient4Config; import org.apache.commons.codec.binary.Base64; @@ -211,12 +212,18 @@ public void testGetBucketInfo() throws Exception { @Test public void testDeleteBucket() throws Exception { + Thread.sleep(1000); // discover all hosts + String bucketName = getTestBucket() + "-x"; Assert.assertFalse("bucket should not exist " + bucketName, client.bucketExists(bucketName)); client.createBucket(bucketName); Assert.assertTrue("failed to create bucket " + bucketName, client.bucketExists(bucketName)); + // write and delete an object + client.putObject(bucketName, "foo", "bar", null); + client.deleteObject(bucketName, "foo"); + client.deleteBucket(bucketName); Assert.assertFalse("failed to delete bucket " + bucketName, client.bucketExists(bucketName)); } @@ -562,6 +569,152 @@ public void testUpdateObjectWithRange() throws Exception { Assert.assertEquals(content.substring(0, offset) + contentPart, client.readObject(getTestBucket(), key, String.class)); } + @Test + public void testGetObjectPreconditions() throws Exception { + String key = "testGetPreconditions"; + String content = "hello GET preconditions!"; + + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, -5); // 5 minutes ago + + client.putObject(getTestBucket(), key, content, "text/plain"); + String etag = client.getObjectMetadata(getTestBucket(), key).getETag(); + + // test if-modified pass + GetObjectRequest request = new GetObjectRequest(getTestBucket(), key); + request.withIfModifiedSince(cal.getTime()); + Assert.assertNotNull(client.getObject(request, String.class)); + + // test if-unmodified fail + request.withIfModifiedSince(null).withIfUnmodifiedSince(cal.getTime()); + Assert.assertNull(client.getObject(request, String.class)); + + // test if-modified fail + cal.add(Calendar.MINUTE, 10); // 5 minutes from now + request.withIfUnmodifiedSince(null).withIfModifiedSince(cal.getTime()); + Assert.assertNull(client.getObject(request, String.class)); + + // test if-unmodified pass + request.withIfModifiedSince(null).withIfUnmodifiedSince(cal.getTime()); + Assert.assertNotNull(client.getObject(request, String.class)); + + // test if-match pass + request.withIfUnmodifiedSince(null).withIfMatch(etag); + Assert.assertNotNull(client.getObject(request, String.class)); + + // test if-none-match fail + request.withIfMatch(null).withIfNoneMatch(etag); + Assert.assertNull(client.getObject(request, String.class)); + + etag = "d41d8cd98f00b204e9800998ecf8427e"; + + // test if-none-match pass + request.withIfNoneMatch(etag); + Assert.assertNotNull(client.getObject(request, String.class)); + + // test if-match fail + request.withIfNoneMatch(null).withIfMatch(etag); + Assert.assertNull(client.getObject(request, String.class)); + } + + // REQUIRES: ECS >= 2.2.1 HF1 + @Test + public void testPutObjectPreconditions() { + String key = "testGetPreconditions"; + String content = "hello GET preconditions!"; + + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, -5); // 5 minutes ago + + client.putObject(getTestBucket(), key, content, "text/plain"); + String etag = client.getObjectMetadata(getTestBucket(), key).getETag(); + + PutObjectRequest request = new PutObjectRequest(getTestBucket(), key, content); + + // test if-unmodified fail + request.withIfUnmodifiedSince(cal.getTime()); + try { + client.putObject(request); + Assert.fail("expected 304"); + } catch (S3Exception e) { + Assert.assertEquals(304, e.getHttpCode()); + } + + // test if-modified pass + request.withIfUnmodifiedSince(null).withIfModifiedSince(cal.getTime()); + client.putObject(request); + + // test if-modified fail + cal.add(Calendar.MINUTE, 10); // 5 minutes from now + request.withIfModifiedSince(cal.getTime()); + try { + client.putObject(request); + Assert.fail("expected 304"); + } catch (S3Exception e) { + Assert.assertEquals(304, e.getHttpCode()); + } + + // test if-unmodified pass + request.withIfModifiedSince(null).withIfUnmodifiedSince(cal.getTime()); + client.putObject(request); + + // test if-match pass + request.withIfUnmodifiedSince(null).withIfMatch(etag); + client.putObject(request); + + // test if-none-match fail + request.withIfMatch(null).withIfNoneMatch(etag); + try { + client.putObject(request); + Assert.fail("expected 304"); + } catch (S3Exception e) { + Assert.assertEquals(304, e.getHttpCode()); + } + + etag = "d41d8cd98f00b204e9800998ecf8427e"; + + // test if-none-match pass + request.withIfNoneMatch(etag); + client.putObject(request); + + // test if-match fail + request.withIfNoneMatch(null).withIfMatch(etag); + try { + client.putObject(request); + Assert.fail("expected 304"); + } catch (S3Exception e) { + Assert.assertEquals(304, e.getHttpCode()); + } + + // test if-match * (if key exists, i.e. update only) pass + request.withIfNoneMatch(null).withIfMatch("*"); + client.putObject(request); + + // test if-none-match * (if key is new, i.e. create only) fail + request.withIfMatch(null).withIfNoneMatch("*"); + try { + client.putObject(request); + Assert.fail("expected 304"); + } catch (S3Exception e) { + Assert.assertEquals(304, e.getHttpCode()); + } + + request.setKey("bogus-key"); + + // test if-match * fail + request.withIfNoneMatch(null).withIfMatch("*"); + try { + client.putObject(request); + Assert.fail("expected 304"); + } catch (S3Exception e) { + Assert.assertEquals(304, e.getHttpCode()); + } + + // test if-none-match * pass + request.withIfMatch(null).withIfNoneMatch("*"); + client.putObject(request); + } + @Test public void testCreateObjectByteArray() throws Exception { byte[] data; @@ -569,6 +722,7 @@ public void testCreateObjectByteArray() throws Exception { data = new byte[15]; random.nextBytes(data); + // FYI, this will set a content-length client.putObject(getTestBucket(), "hello-bytes-small", data, null); Assert.assertArrayEquals(data, client.readObject(getTestBucket(), "hello-bytes-small", byte[].class)); @@ -583,6 +737,21 @@ public void testCreateObjectByteArray() throws Exception { Assert.assertArrayEquals(data, client.readObject(getTestBucket(), "hello-bytes-more", byte[].class)); } + @Test + public void testCreateObjectWithStream() throws Exception { + byte[] data = new byte[100]; + Random random = new Random(); + random.nextBytes(data); + + // FYI, this will set a content-length + client.putObject(getTestBucket(), "byte-array-test", new ByteArrayInputStream(data), null); + Assert.assertArrayEquals(data, client.readObject(getTestBucket(), "byte-array-test", byte[].class)); + + // ... and this will use chunked-encoding + client.putObject(getTestBucket(), "random-array-test", new RandomInputStream(100), null); + Assert.assertEquals(new Long(100), client.getObjectMetadata(getTestBucket(), "random-array-test").getContentLength()); + } + @Test public void testCreateObjectString() throws Exception { String key = "string-test"; @@ -605,11 +774,11 @@ public void testCreateObjectChunkedWithRequest() throws Exception { new Random().nextBytes(data); String dataStr = new String(data); PutObjectRequest request = new PutObjectRequest(getTestBucket(), "/objectPrefix/testObject1", dataStr); - //request.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED); + //request.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED); //request.property(ClientConfig.PROPERTY_CHUNKED_ENCODING_SIZE, -1); + //request.property(ApacheHttpClient4Config.PROPERTY_ENABLE_BUFFERING, Boolean.FALSE); - request.property(ApacheHttpClient4Config.PROPERTY_ENABLE_BUFFERING, Boolean.FALSE); PutObjectResult result = client.putObject(request); Assert.assertNotNull(result); } @@ -639,6 +808,38 @@ public void testCreateObjectWithMetadata() throws Exception { Assert.assertEquals(userMeta, objectMetadata.getUserMetadata()); } + @Test + public void testCreateObjectWithRetentionPeriod() throws Exception { + String key = "object-in-retention"; + String content = "Hello Retention!"; + S3ObjectMetadata objectMetadata = new S3ObjectMetadata(); + objectMetadata.setRetentionPeriod(2L); + client.putObject(new PutObjectRequest(getTestBucket(), key, content).withObjectMetadata(objectMetadata)); + objectMetadata = client.getObjectMetadata(getTestBucket(), key); + Assert.assertEquals((Long) 2L, objectMetadata.getRetentionPeriod()); + Assert.assertEquals(content, client.readObject(getTestBucket(), key, String.class)); + try { + client.putObject(getTestBucket(), key, "evil update!", null); + Assert.fail("object in retention allowed update"); + } catch (S3Exception e) { + Assert.assertEquals("ObjectUnderRetention", e.getErrorCode()); + } + + Thread.sleep(5000); // allow retention to expire + client.putObject(getTestBucket(), key, "good update!", null); + } + + @Test + public void testCreateObjectWithRetentionPolicy() throws Exception { + String key = "object-in-retention-policy"; + String content = "Hello Retention Policy!"; + S3ObjectMetadata objectMetadata = new S3ObjectMetadata(); + objectMetadata.setRetentionPolicy("bad-policy"); + client.putObject(new PutObjectRequest(getTestBucket(), key, content).withObjectMetadata(objectMetadata)); + + // no way to verify, so if no error is returned, assume success + } + @Test public void testLargeObjectContentLength() throws Exception { String key = "large-object"; diff --git a/src/test/java/com/emc/object/s3/S3MetadataSearchTest.java b/src/test/java/com/emc/object/s3/S3MetadataSearchTest.java index aebd8899..78a0400c 100644 --- a/src/test/java/com/emc/object/s3/S3MetadataSearchTest.java +++ b/src/test/java/com/emc/object/s3/S3MetadataSearchTest.java @@ -52,9 +52,16 @@ public void testListSystemMetadataSearchKeys() throws Exception { MetadataSearchKey[] expectedOptionalAttributes = new MetadataSearchKey[] { new MetadataSearchKey("ContentEncoding", MetadataSearchDatatype.string), new MetadataSearchKey("ContentType", MetadataSearchDatatype.string), + new MetadataSearchKey("CreateTime", MetadataSearchDatatype.datetime), + new MetadataSearchKey("Etag", MetadataSearchDatatype.string), new MetadataSearchKey("Expiration", MetadataSearchDatatype.datetime), new MetadataSearchKey("Expires", MetadataSearchDatatype.datetime), + new MetadataSearchKey("LastModified", MetadataSearchDatatype.datetime), + new MetadataSearchKey("Namespace", MetadataSearchDatatype.string), + new MetadataSearchKey("ObjectName", MetadataSearchDatatype.string), + new MetadataSearchKey("Owner", MetadataSearchDatatype.string), new MetadataSearchKey("Retention", MetadataSearchDatatype.integer), + new MetadataSearchKey("Size", MetadataSearchDatatype.integer), }; MetadataSearchList list = client.listSystemMetadataSearchKeys(); diff --git a/src/test/java/com/emc/object/s3/bean/QueryObjectResultTest.java b/src/test/java/com/emc/object/s3/bean/QueryObjectResultTest.java new file mode 100644 index 00000000..2602566c --- /dev/null +++ b/src/test/java/com/emc/object/s3/bean/QueryObjectResultTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2015-2016, EMC Corporation. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * + Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + The name of EMC Corporation may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.emc.object.s3.bean; + +import org.junit.Assert; +import org.junit.Test; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.*; + +public class QueryObjectResultTest { + @Test + public void testMarshalling() throws Exception { + JAXBContext context = JAXBContext.newInstance(QueryObjectsResult.class); + + String xml = "" + + "" + + "s3-metadata-search-test-arnetc-26388" + + "NO MORE PAGES" + + "1000" + + "" + + "" + + "object1" + + "5c5e56696ee4413109b37a4e3e602032c3642378e410b90c4f19e4b08fb1ec16" + + "0" + + "SYSMD" + + "" + + "ctypeapplication/octet-stream" + + "size0" + + "" + + "" + + "USERMD" + + "" + + "x-amz-meta-datetime12015-01-01T00:00:00Z" + + "x-amz-meta-decimal13.14159" + + "x-amz-meta-integer142" + + "x-amz-meta-string1test" + + "" + + "" + + "" + + "" + + ""; + + QueryObjectsResult result = new QueryObjectsResult(); + result.setBucketName("s3-metadata-search-test-arnetc-26388"); + result.setNextMarker("NO MORE PAGES"); + result.setMaxKeys(1000); + + List objects = new ArrayList(); + + QueryObject object = new QueryObject(); + object.setObjectName("object1"); + object.setObjectId("5c5e56696ee4413109b37a4e3e602032c3642378e410b90c4f19e4b08fb1ec16"); + object.setVersionId("0"); + + List queryMds = new ArrayList(); + + QueryMetadata metadata = new QueryMetadata(); + metadata.setType(QueryMetadataType.SYSMD); + Map mdMap = new HashMap(); + mdMap.put("ctype", "application/octet-stream"); + mdMap.put("size", "0"); + metadata.setMdMap(mdMap); + queryMds.add(metadata); + + metadata = new QueryMetadata(); + metadata.setType(QueryMetadataType.USERMD); + mdMap = new TreeMap(); + mdMap.put("x-amz-meta-datetime1", "2015-01-01T00:00:00Z"); + mdMap.put("x-amz-meta-decimal1", "3.14159"); + mdMap.put("x-amz-meta-integer1", "42"); + mdMap.put("x-amz-meta-string1", "test"); + metadata.setMdMap(mdMap); + queryMds.add(metadata); + + object.setQueryMds(queryMds); + objects.add(object); + + result.setObjects(objects); + + // unmarshall and compare to object + Unmarshaller unmarshaller = context.createUnmarshaller(); + QueryObjectsResult unmarshalledObject = (QueryObjectsResult) unmarshaller.unmarshal(new StringReader(xml)); + + Assert.assertEquals(result.getBucketName(), unmarshalledObject.getBucketName()); + Assert.assertEquals(result.getNextMarker(), unmarshalledObject.getNextMarker()); + Assert.assertEquals(result.getMaxKeys(), unmarshalledObject.getMaxKeys()); + + for (QueryObject o : result.getObjects()) { + Assert.assertTrue(unmarshalledObject.getObjects().contains(o)); + } + for (QueryObject o : unmarshalledObject.getObjects()) { + Assert.assertTrue(result.getObjects().contains(o)); + } + + // re-marshall and compare to string + Marshaller marshaller = context.createMarshaller(); + StringWriter writer = new StringWriter(); + marshaller.marshal(result, writer); + Assert.assertEquals(xml, writer.toString()); + } +}