/*************************************************************************
* Copyright 2009-2015 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.objectstorage.entities;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.PersistenceContext;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import javax.persistence.Transient;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import org.apache.log4j.Logger;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.annotations.OptimisticLockType;
import org.hibernate.annotations.OptimisticLocking;
import org.hibernate.annotations.Type;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Restrictions;
import com.eucalyptus.auth.principal.UserPrincipal;
import com.eucalyptus.objectstorage.ObjectState;
import com.eucalyptus.objectstorage.util.ObjectStorageProperties;
import com.eucalyptus.storage.common.DateFormatter;
import com.eucalyptus.storage.msgs.s3.CanonicalUser;
import com.eucalyptus.storage.msgs.s3.DeleteMarkerEntry;
import com.eucalyptus.storage.msgs.s3.KeyEntry;
import com.eucalyptus.storage.msgs.s3.ListEntry;
import com.eucalyptus.storage.msgs.s3.VersionEntry;
import com.google.common.collect.Maps;
@Entity
@OptimisticLocking(type = OptimisticLockType.NONE)
@PersistenceContext(name = "eucalyptus_osg")
@Table(name = "objects", indexes = {
@Index(name = "IDX_object_key", columnList = "object_key"),
@Index(name = "IDX_object_uuid", columnList = "object_uuid"),
@Index(name = "IDX_version_id", columnList = "version_id"),
})
public class ObjectEntity extends S3AccessControlledEntity<ObjectState> implements Comparable {
@Transient
private static Logger LOG = Logger.getLogger(ObjectEntity.class);
@Column(name = "object_key")
private String objectKey;
@NotFound(action = NotFoundAction.EXCEPTION)
@ManyToOne(optional = false, targetEntity = Bucket.class, fetch = FetchType.EAGER)
@ForeignKey(name = "FK_bucket")
@JoinColumn(name = "bucket_fk")
private Bucket bucket;
@Column(name = "object_uuid", unique = true, nullable = false)
private String objectUuid; // The a uuid for this specific object content & request
@Column(name = "version_id", nullable = false)
private String versionId; // VersionId is required to uniquely identify ACLs and auth
@Column(name = "size", nullable = false)
private Long size;
@Column(name = "storage_class", nullable = false)
private String storageClass;
@Column(name = "is_delete_marker")
private Boolean isDeleteMarker; // Indicates this is a delete marker
@Column(name = "object_last_modified")
// Distinct from the record modification date, tracks the backend response
private Date objectModifiedTimestamp;
@Column(name = "etag")
private String eTag;
@Column(name = "is_latest")
private Boolean isLatest;
@Column(name = "creation_expiration")
private Long creationExpiration; // Expiration time in system/epoch time to guarantee monotonically increasing values
@Column(name = "upload_id")
private String uploadId;
@Column(name = "stored_headers")
@Lob
@Type(type = "org.hibernate.type.StringClobType")
private String storedHeaders;
public Long getCreationExpiration() {
return creationExpiration;
}
public void setCreationExpiration(Long creationExpiration) {
this.creationExpiration = creationExpiration;
}
public Boolean getIsDeleteMarker() {
return isDeleteMarker;
}
public void setIsDeleteMarker(Boolean isDeleteMarker) {
this.isDeleteMarker = isDeleteMarker;
}
public ObjectEntity() {
super();
}
public ObjectEntity(Bucket parentBucket, String objectKey, String versionId) {
super();
this.bucket = parentBucket;
this.objectKey = objectKey;
this.versionId = versionId;
}
public ObjectEntity withBucket(Bucket parentBucket) {
this.setBucket(parentBucket);
return this;
}
public ObjectEntity withKey(String key) {
this.setObjectKey(key);
return this;
}
public ObjectEntity withVersionId(String versionId) {
this.setVersionId(versionId);
return this;
}
public ObjectEntity withUuid(String uuid) {
this.setObjectUuid(uuid);
return this;
}
public ObjectEntity withUploadId(String uploadId) {
this.setUploadId(uploadId);
return this;
}
/**
* Sets state only, explicitly nulls 'lastState'. Use ONLY for query examples
*
* @param s
* @return
*/
public ObjectEntity withState(ObjectState s) {
this.setState(s);
this.setLastState(null);
return this;
}
public Bucket getBucket() {
return bucket;
}
public void setBucket(Bucket bucket) {
this.bucket = bucket;
}
@PrePersist
public void ensureFieldsNotNull() {
if (this.versionId == null) {
this.versionId = ObjectStorageProperties.NULL_VERSION_ID;
}
if (this.isDeleteMarker == null) {
this.isDeleteMarker = false;
}
if (this.objectUuid == null) {
// generate a new one
this.generateInternalKey(this.objectKey);
}
}
/**
* Initialize this as a new object entity representing an object to PUT
*
* @param bucket
* @param objectKey
* @param usr
*/
public static ObjectEntity newInitializedForCreate(@Nonnull Bucket bucket, @Nonnull String objectKey, long contentLength, @Nonnull UserPrincipal usr)
throws Exception {
ObjectEntity entity = new ObjectEntity(bucket, objectKey, bucket.generateObjectVersionId());
entity.setObjectUuid(generateInternalKey(objectKey));
entity.setOwnerCanonicalId(usr.getCanonicalId());
entity.setOwnerDisplayName(usr.getAccountAlias());
entity.setOwnerIamUserId(usr.getUserId());
entity.setOwnerIamUserDisplayName(usr.getName());
entity.setObjectModifiedTimestamp(null);
entity.setSize(contentLength);
entity.setIsLatest(false);
entity.setStorageClass(ObjectStorageProperties.STORAGE_CLASS.STANDARD.toString());
entity.updateCreationExpiration();
entity.setState(ObjectState.creating);
return entity;
}
public static ObjectEntity newInitializedForCreate(@Nonnull Bucket bucket, @Nonnull String objectKey, long contentLength,
@Nonnull UserPrincipal usr, @Nullable Map<String, String> headersToStore) throws Exception {
ObjectEntity entity = newInitializedForCreate(bucket, objectKey, contentLength, usr);
entity.setStoredHeaders(headersToStore);
return entity;
}
public void updateCreationExpiration() {
this.creationExpiration = System.currentTimeMillis() + (1000 * ObjectStorageProperties.OBJECT_CREATION_EXPIRATION_INTERVAL_SEC);
}
/**
* Creates a new 'DeleteMarker' object entity from this object
*
* @return
* @throws Exception
*/
public ObjectEntity generateNewDeleteMarkerFrom() {
ObjectEntity deleteMarker =
new ObjectEntity(this.bucket, this.getObjectKey(), this.getBucket().generateObjectVersionId()).withState(ObjectState.creating);
deleteMarker.setObjectUuid(generateInternalKey(objectKey));
deleteMarker.setStorageClass("STANDARD");
deleteMarker.setObjectModifiedTimestamp(new Date());
deleteMarker.setIsDeleteMarker(true);
deleteMarker.setSize(0L);
deleteMarker.setIsLatest(true);
return deleteMarker;
}
private static String generateInternalKey(@Nonnull String key) {
return UUID.randomUUID().toString(); // Use only uuid to ensure key length max met
}
public String geteTag() {
return eTag;
}
public void seteTag(String eTag) {
this.eTag = eTag;
}
public Date getObjectModifiedTimestamp() {
return objectModifiedTimestamp;
}
public void setObjectModifiedTimestamp(Date objectModifiedTimestamp) {
this.objectModifiedTimestamp = objectModifiedTimestamp;
}
@Override
public String getResourceFullName() {
return getBucket().getBucketName() + "/" + getObjectKey();
}
public String getObjectKey() {
return objectKey;
}
public void setObjectKey(String objectKey) {
this.objectKey = objectKey;
}
public Long getSize() {
return size;
}
public void setSize(Long size) {
this.size = size;
}
public String getStorageClass() {
return storageClass;
}
public void setStorageClass(String storageClass) {
this.storageClass = storageClass;
}
public String getVersionId() {
return versionId;
}
public void setVersionId(String versionId) {
this.versionId = versionId;
}
public int compareTo(Object o) {
return this.objectKey.compareTo(((ObjectEntity) o).getObjectKey());
}
public boolean isPending() {
return (getObjectModifiedTimestamp() == null);
}
public String getObjectUuid() {
return objectUuid;
}
public void setObjectUuid(String objectUuid) {
this.objectUuid = objectUuid;
}
public Boolean getIsLatest() {
return isLatest;
}
public void setIsLatest(Boolean isLatest) {
this.isLatest = isLatest;
}
public boolean isNullVersioned() {
return ObjectStorageProperties.NULL_VERSION_ID.equals(this.versionId);
}
public String getUploadId() {
return uploadId;
}
public void setUploadId(String uploadId) {
this.uploadId = uploadId;
}
public Map<String, String> getStoredHeaders() {
Map<String, String> headersMap = Maps.newHashMap();
if (storedHeaders == null || "".equals(storedHeaders)) {
return headersMap;
}
JSONObject headersJson = (JSONObject) JSONSerializer.toJSON(this.storedHeaders);
String key = null;
Iterator keys = headersJson.keys();
while (keys.hasNext()) {
key = (String) keys.next();
Object value = headersJson.get(key);
if (value != null) {
headersMap.put(key, value.toString());
}
}
return headersMap;
}
public void setStoredHeaders(Map<String, String> storedHeadersMap) {
this.storedHeaders = JSONObject.fromObject(storedHeadersMap).toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((objectUuid == null) ? 0 : objectUuid.hashCode());
result = prime * result + ((objectKey == null) ? 0 : objectKey.hashCode());
result = prime * result + ((versionId == null) ? 0 : versionId.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ObjectEntity other = (ObjectEntity) obj;
if (bucket == null) {
if (other.bucket == null)
return false;
} else if (!bucket.equals(other.bucket))
return false;
if (objectKey == null) {
if (other.objectKey != null)
return false;
} else if (!objectKey.equals(other.objectKey))
return false;
if (versionId == null) {
if (other.versionId != null)
return false;
} else if (!versionId.equals(other.versionId))
return false;
if (objectUuid == null) {
if (other.objectUuid != null)
return false;
} else if (!objectUuid.equals(other.objectUuid))
return false;
return true;
}
public static class QueryHelpers {
public static Criterion getIsPartRestriction() {
return Restrictions.isNotNull("partNumber");
}
public static Criterion getIsNotPartRestriction() {
return Restrictions.isNull("partNumber");
}
public static Criterion getIsPendingRestriction() {
return Restrictions.isNull("objectModifiedTimestamp");
}
public static Criterion getIsMultipartRestriction() {
return Restrictions.isNotNull("uploadId");
}
}
/**
* Return a ListEntry for this entity
*
* @return
*/
public ListEntry toListEntry() {
ListEntry e = new ListEntry();
e.setEtag("\"" + this.geteTag() + "\"");
e.setKey(this.getObjectKey());
e.setLastModified(DateFormatter.dateToListingFormattedString(this.getObjectModifiedTimestamp()));
e.setSize(this.getSize());
e.setStorageClass(this.getStorageClass());
e.setOwner(new CanonicalUser(this.getOwnerCanonicalId(), this.getOwnerDisplayName()));
return e;
}
/**
* Return a VersionEntry for this entity
*
* @return
*/
public KeyEntry toVersionEntry() {
if (!this.isDeleteMarker) {
VersionEntry e = new VersionEntry();
e.setEtag("\"" + this.geteTag() + "\"");
e.setKey(this.getObjectKey());
e.setVersionId(this.getVersionId());
e.setLastModified(DateFormatter.dateToListingFormattedString(this.getObjectModifiedTimestamp()));
e.setSize(this.getSize());
e.setIsLatest(this.isLatest);
e.setStorageClass(this.getStorageClass());
e.setOwner(new CanonicalUser(this.getOwnerCanonicalId(), this.getOwnerDisplayName()));
return e;
} else {
DeleteMarkerEntry e = new DeleteMarkerEntry();
e.setKey(this.getObjectKey());
e.setVersionId(this.getVersionId());
e.setLastModified(DateFormatter.dateToListingFormattedString(this.getObjectModifiedTimestamp()));
e.setIsLatest(this.isLatest);
e.setOwner(new CanonicalUser(this.getOwnerCanonicalId(), this.getOwnerDisplayName()));
return e;
}
}
// TODO: add delete marker support. Fix is to use super-type for versioning entry and sub-types for version vs deleteMarker
}