org.jets3t.service.model.S3Object.java Source code

Java tutorial

Introduction

Here is the source code for org.jets3t.service.model.S3Object.java

Source

/*
 * jets3t : Java Extra-Tasty S3 Toolkit (for Amazon S3 online storage service)
 * This is a java.net project, see https://jets3t.dev.java.net/
 * 
 * Copyright 2006 James Murty
 * 
 * Licensed 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.jets3t.service.model;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jets3t.service.Constants;
import org.jets3t.service.S3ServiceException;
import org.jets3t.service.acl.AccessControlList;
import org.jets3t.service.io.RepeatableFileInputStream;
import org.jets3t.service.utils.Mimetypes;
import org.jets3t.service.utils.ServiceUtils;

/**
 * An S3 object.
 * 
 * @author James Murty
 */
public class S3Object extends BaseS3Object implements Cloneable {
    private static final Log log = LogFactory.getLog(S3Object.class);

    private static final long serialVersionUID = -2883501141593631181L;

    /*
     * Listing of the metadata names that are required in S3 objects, or are used frequently
     * in jets3t applications.  
     */
    public static final String METADATA_HEADER_LAST_MODIFIED_DATE = "Last-Modified";
    public static final String METADATA_HEADER_DATE = "Date";
    public static final String METADATA_HEADER_OWNER = "Owner";
    public static final String METADATA_HEADER_ETAG = "ETag";
    public static final String METADATA_HEADER_HASH_MD5 = "md5-hash";
    public static final String METADATA_HEADER_ORIGINAL_HASH_MD5 = "original-md5-hash";
    public static final String METADATA_HEADER_CONTENT_MD5 = "Content-MD5";
    public static final String METADATA_HEADER_CONTENT_LENGTH = "Content-Length";
    public static final String METADATA_HEADER_CONTENT_TYPE = "Content-Type";
    public static final String METADATA_HEADER_CONTENT_ENCODING = "Content-Encoding";
    public static final String METADATA_HEADER_STORAGE_CLASS = "Storage-Class";
    public static final String METADATA_HEADER_CONTENT_DISPOSITION = "Content-Disposition";
    public static final String METADATA_HEADER_CONTENT_LANGUAGE = "Content-Language";

    private String key = null;
    private String bucketName = null;
    private transient InputStream dataInputStream = null;
    private AccessControlList acl = null;
    private boolean isMetadataComplete = false;

    /**
     * Store references to files when the object's data comes from a file, to allow for lazy
     * opening of the file's input streams.
     */
    private File dataInputFile = null;

    /**
     * Create an object representing a file. The object is initialised with the file's name
     * as its key, the file's content as its data, a content type based on the file's extension
     * (see {@link Mimetypes}), and a content length matching the file's size.
     * The file's MD5 hash value is also calculated and provided to S3, so the service
     * can verify that no data are corrupted in transit.
     * 
     * @param bucket
     * the bucket the object belongs to, or will be placed in.
     * @param file
     * the file the object will represent. This file must exist and be readable.
     * 
     * @throws IOException when an i/o error occurred reading the file
     * @throws NoSuchAlgorithmException when this JRE doesn't support the MD5 hash algorithm 
     */
    public S3Object(S3Bucket bucket, File file) throws NoSuchAlgorithmException, IOException {
        this(bucket, file.getName());
        setContentLength(file.length());
        setContentType(Mimetypes.getInstance().getMimetype(file));
        if (!file.exists()) {
            throw new FileNotFoundException("Cannot read from file: " + file.getAbsolutePath());
        }
        setDataInputFile(file);
        setMd5Hash(ServiceUtils.computeMD5Hash(new FileInputStream(file)));
    }

    /**
     * Create an object representing text data. The object is initialized with the given
     * key, the given string as its data content (encoded as UTF-8), a content type of 
     * <code>text/plain; charset=utf-8</code>, and a content length matching the 
     * string's length.
     * The given string's MD5 hash value is also calculated and provided to S3, so the service
     * can verify that no data are corrupted in transit.
     * 
     * @param bucket
     * the bucket the object belongs to, or will be placed in.
     * @param key
     * the key name for the object.
     * @param dataString
     * the text data the object will contain. Text data will be encoded as UTF-8. 
     * This string cannot be null.
     * 
     * @throws IOException 
     * @throws NoSuchAlgorithmException when this JRE doesn't support the MD5 hash algorithm 
     */
    public S3Object(S3Bucket bucket, String key, String dataString) throws NoSuchAlgorithmException, IOException {
        this(bucket, key);
        ByteArrayInputStream bais = new ByteArrayInputStream(dataString.getBytes(Constants.DEFAULT_ENCODING));
        setDataInputStream(bais);
        setContentLength(bais.available());
        setContentType("text/plain; charset=utf-8");
        setMd5Hash(ServiceUtils.computeMD5Hash(dataString.getBytes(Constants.DEFAULT_ENCODING)));
    }

    /**
     * Create an object without any associated data, and no associated bucket.
     * 
     * @param key
     * the key name for the object.
     */
    public S3Object(String key) {
        this.key = key;
    }

    /**
     * Create an object without any associated data.
     * 
     * @param bucket
     * the bucket the object belongs to, or will be placed in.
     * @param key
     * the key name for the object.
     */
    public S3Object(S3Bucket bucket, String key) {
        if (bucket != null) {
            this.bucketName = bucket.getName();
        }
        this.key = key;
    }

    public String toString() {
        return "S3Object [key=" + getKey() + ",bucket=" + (bucketName == null ? "<Unknown>" : bucketName)
                + ",lastModified=" + getLastModifiedDate() + ", dataInputStream=" + dataInputStream + "] Metadata="
                + getMetadataMap();
    }

    /**
     * Returns an input stream containing this object's data, or null if there is 
     * no data associated with the object.
     * <p>
     * If you are downloading data from S3, you should consider verifying the
     * integrity of the data you read from this stream using one of the  
     * {@link #verifyData(InputStream)} methods.  
     * 
     * @throws S3ServiceException 
     */
    public InputStream getDataInputStream() throws S3ServiceException {
        if (dataInputStream == null && dataInputFile != null) {
            try {
                // Use a repeatable file data input stream, so transmissions can be retried if necessary.
                dataInputStream = new RepeatableFileInputStream(dataInputFile);
            } catch (FileNotFoundException e) {
                throw new S3ServiceException("Cannot open file input stream", e);
            }
        }
        return dataInputStream;
    }

    /**
     * Sets an input stream containing the data content to associate with this object.
     * <p>
     * <b>Note</b>: If the data content comes from a file, use the alternate method
     * {@link #setDataInputFile(File)} which allows objects to lazily open files and avoid any
     * Operating System limits on the number of files that may be opened simultaneously. 
     * <p>
     * <b>Note 2</b>: This method does not calculate an MD5 hash of the input data, 
     * which means S3 will not be able to recognize if data are corrupted in transit. 
     * To allow S3 to verify data you upload, you should set the MD5 hash value of
     * your data using {@link #setMd5Hash(byte[])}.
     * <p>
     * This method will set the object's file data reference to null.
     * 
     * @param dataInputStream
     * an input stream containing the object's data.
     */
    public void setDataInputStream(InputStream dataInputStream) {
        this.dataInputFile = null;
        this.dataInputStream = dataInputStream;
    }

    /**
     * Sets the file containing the data content to associate with this object. This file will
     * be automatically opened as an input stream only when absolutely necessary, that is when
     * {@link #getDataInputStream()} is called.
     * <p>
     * <b>Note 2</b>: This method does not calculate an MD5 hash of the input data, 
     * which means S3 will not be able to recognize if data are corrupted in transit. 
     * To allow S3 to verify data you upload, you should set the MD5 hash value of
     * your data using {@link #setMd5Hash(byte[])}.
     * <p>
     * This method will set the object's input stream data reference to null.
     * 
     * @param dataInputFile
     * a file containing the object's data.
     */
    public void setDataInputFile(File dataInputFile) {
        this.dataInputStream = null;
        this.dataInputFile = dataInputFile;
    }

    /**
     * Closes the object's data input stream if it exists.
     * 
     * @throws IOException
     */
    public void closeDataInputStream() throws IOException {
        if (this.dataInputStream != null) {
            this.dataInputStream.close();
            this.dataInputStream = null;
        }
    }

    /**
     * @return
     * the ETag value of the object as returned by S3 when an object is created. The ETag value
     * does not include quote (") characters. This value will be null if the object's ETag value
     * is not known, such as when an object has not yet been uploaded to S3.
     */
    public String getETag() {
        String etag = (String) getMetadata(METADATA_HEADER_ETAG);
        if (etag != null) {
            if (etag.startsWith("\"") && etag.endsWith("\"")) {
                return etag.substring(1, etag.length() - 1);
            }
        }
        return etag;
    }

    /**
     * Set the ETag value of the object based on information returned from S3.
     * This method should only by used by code that reads S3 responses.
     * 
     * @param etag
     * the ETag value as provided by S3.
     */
    public void setETag(String etag) {
        addMetadata(METADATA_HEADER_ETAG, etag);
    }

    /**
     * @return
     * the hex-encoded MD5 hash of an object's data contents as stored in the jets3t-specific metadata
     * item <code>md5-hash</code>, or null if the hash value is not available.
     */
    public String getMd5HashAsHex() {
        return (String) getMetadata(METADATA_HEADER_HASH_MD5);
    }

    /**
     * @return
     * the Base64-encoded MD5 hash of an object's data contents as stored in the metadata
     * item <code>Content-MD5</code>, or as derived from an <code>ETag</code> or 
     * <code>md5-hash</code> hex-encoded version of the hash. Returns null if the hash value is not 
     * available.
     */
    public String getMd5HashAsBase64() {
        String md5HashBase64 = (String) getMetadata(METADATA_HEADER_CONTENT_MD5);
        if (md5HashBase64 == null) {
            // Try converting the object's ETag (a hex-encoded md5 hash).
            if (getETag() != null) {
                return ServiceUtils.toBase64(ServiceUtils.fromHex(getETag()));
            }
            // Try converting the object's md5-hash (another hex-encoded md5 hash).
            if (getMd5HashAsHex() != null) {
                return ServiceUtils.toBase64(ServiceUtils.fromHex(getMd5HashAsHex()));
            }
        }
        return md5HashBase64;
    }

    /**
     * Set the MD5 hash value of this object's data.
     * The hash value is stored as metadata under <code>Content-MD5</code> (Base64-encoded)
     * and the jets3t-specific <code>md5-hash</code> (Hex-encoded).
     * 
     * @param md5Hash
     * the MD5 hash value of the object's data.
     */
    public void setMd5Hash(byte[] md5Hash) {
        addMetadata(METADATA_HEADER_HASH_MD5, ServiceUtils.toHex(md5Hash));
        addMetadata(METADATA_HEADER_CONTENT_MD5, ServiceUtils.toBase64(md5Hash));
    }

    /**
     * @return
     * the last modified date of this object, as provided by S3. If the last modified date is not
     * available (e.g. if the object has only just been created) the object's creation date is 
     * returned instead. If both last modified and creation dates are unavailable, null is returned.
     */
    public Date getLastModifiedDate() {
        Date lastModifiedDate = (Date) getMetadata(METADATA_HEADER_LAST_MODIFIED_DATE);
        if (lastModifiedDate == null) {
            // Perhaps this object has just been created, in which case we can use the Date metadata.
            lastModifiedDate = (Date) getMetadata(METADATA_HEADER_DATE);
        }
        return lastModifiedDate;
    }

    /**
     * Set this object's last modified date based on information returned from S3.
     * This method should only by used internally by code that reads the last modified date
     * from an S3 response; it must not be set prior to uploading data to S3.
     * 
     * @param lastModifiedDate
     */
    public void setLastModifiedDate(Date lastModifiedDate) {
        addMetadata(METADATA_HEADER_LAST_MODIFIED_DATE, lastModifiedDate);
    }

    /**
     * @return
     * this object's owner, or null if the owner is not available.
     */
    public S3Owner getOwner() {
        return (S3Owner) getMetadata(METADATA_HEADER_OWNER);
    }

    /**
     * Set this object's owner object based on information returned from S3.
     * This method should only by used by code that reads S3 responses.
     * 
     * @param owner
     */
    public void setOwner(S3Owner owner) {
        addMetadata(METADATA_HEADER_OWNER, owner);
    }

    /**
     * @return
     * the content length, or size, of this object's data, or 0 if it is unknown.
     */
    public long getContentLength() {
        Object contentLength = getMetadata(METADATA_HEADER_CONTENT_LENGTH);
        if (contentLength == null) {
            return 0;
        } else {
            return Long.parseLong(contentLength.toString());
        }
    }

    /**
     * Set this object's content length. The content length is set internally by JetS3t for
     * objects that are retrieved from S3. For objects that are uploaded into S3, JetS3t
     * automatically calculates the content length if the data is provided to the String- or 
      * File-based S3Object constructor. If you manually provide data to this object via the
      * {@link #setDataInputStream(InputStream)} or {@link #setDataInputFile(File)} methods, 
      * you must also set the content length value.
     * @param size
     */
    public void setContentLength(long size) {
        addMetadata(METADATA_HEADER_CONTENT_LENGTH, String.valueOf(size));
    }

    /**
     * @return
     * the storage class of the object. 
     */
    public String getStorageClass() {
        return (String) getMetadata(METADATA_HEADER_STORAGE_CLASS);
    }

    /**
     * Set the storage class based on information returned from S3.
     * This method should only by used by code that reads S3 responses.
     * 
     * @param storageClass
     */
    public void setStorageClass(String storageClass) {
        addMetadata(METADATA_HEADER_STORAGE_CLASS, storageClass);
    }

    /**
     * @return
     * the content type of the object
     */
    public String getContentType() {
        return (String) getMetadata(METADATA_HEADER_CONTENT_TYPE);
    }

    /**
     * Set the content type of the object. JetS3t can help you determine the 
      * content type when the associated data is a File (see {@link Mimetypes}). 
     * You should set the content type for associated String or InputStream data.
     * 
     * @param contentType
     */
    public void setContentType(String contentType) {
        addMetadata(METADATA_HEADER_CONTENT_TYPE, contentType);
    }

    /**
     * @return
     * the content language of this object, or null if it is unknown.
     */
    public String getContentLanguage() {
        return (String) getMetadata(METADATA_HEADER_CONTENT_LANGUAGE);
    }

    /**
     * Set the content language of the object. 
     * @param contentLanguage
     */
    public void setContentLanguage(String contentLanguage) {
        addMetadata(METADATA_HEADER_CONTENT_LANGUAGE, contentLanguage);
    }

    /**
     * @return
     * the content disposition of this object, or null if it is unknown.
     */
    public String getContentDisposition() {
        return (String) getMetadata(METADATA_HEADER_CONTENT_DISPOSITION);
    }

    /**
     * Set the content disposition of the object.
     * @param contentDisposition
     */
    public void setContentDisposition(String contentDisposition) {
        addMetadata(METADATA_HEADER_CONTENT_DISPOSITION, contentDisposition);
    }

    /**
     * @return
     * the content encoding of this object, or null if it is unknown.
     */
    public String getContentEncoding() {
        return (String) getMetadata(METADATA_HEADER_CONTENT_ENCODING);
    }

    /**
     * Set the content encoding of this object.
     * @param contentEncoding
     */
    public void setContentEncoding(String contentEncoding) {
        addMetadata(METADATA_HEADER_CONTENT_ENCODING, contentEncoding);
    }

    /**
     * @return
     * the name of the bucket this object belongs to or will be placed into, or null if none is set.
     */
    public String getBucketName() {
        return bucketName;
    }

    /**
     * Set the name of the bucket this object belongs to or will be placed into.
     * @param bucketName the name for the bucket.
     */
    public void setBucketName(String bucketName) {
        this.bucketName = bucketName;
    }

    /**
     * @return
     * the object's ACL, or null if it is unknown.
     */
    public AccessControlList getAcl() {
        return acl;
    }

    /**
     * Set the object's ACL. If a pre-canned REST ACL is used, the plain-text representation
     * of the canned ACL is also added as a metadata header <code>x-amz-acl</code>. 
     * 
     * @param acl
     */
    public void setAcl(AccessControlList acl) {
        this.acl = acl;

        if (acl != null) {
            if (AccessControlList.REST_CANNED_PRIVATE.equals(acl)) {
                addMetadata(Constants.REST_HEADER_PREFIX + "acl", "private");
            } else if (AccessControlList.REST_CANNED_PUBLIC_READ.equals(acl)) {
                addMetadata(Constants.REST_HEADER_PREFIX + "acl", "public-read");
            } else if (AccessControlList.REST_CANNED_PUBLIC_READ_WRITE.equals(acl)) {
                addMetadata(Constants.REST_HEADER_PREFIX + "acl", "public-read-write");
            } else if (AccessControlList.REST_CANNED_AUTHENTICATED_READ.equals(acl)) {
                addMetadata(Constants.REST_HEADER_PREFIX + "acl", "authenticated-read");
            } else {
                // Non-REST canned ACLs are not added as headers...
            }
        }
    }

    /**
     * @return
     * the key of this object.
     */
    public String getKey() {
        return key;
    }

    /**
     * Set the key of this object.
     * @param key the key for this object.
     */
    public void setKey(String key) {
        this.key = key;
    }

    /**
     * @return
     * true if the object's metadata are considered complete, such as when the object's metadata
     * has been retrieved from S3 by a HEAD request. If this value is not true, the metadata
     * information in this object should not be considered authoritative.
     */
    public boolean isMetadataComplete() {
        return isMetadataComplete;
    }

    /**
     * S3 Object metadata are only complete when it is populated with all values following
     * a HEAD or GET request.
     * This method should only by used by code that reads S3 responses.
     * 
     * @param isMetadataComplete
     */
    public void setMetadataComplete(boolean isMetadataComplete) {
        this.isMetadataComplete = isMetadataComplete;
    }

    /**
     * Add metadata information to the object. If date metadata items (as recognized by name)
     * are added and the value is not a date, the value is parsed as an ISO 8601 string.
     * @param name
     * @param value
     */
    public void addMetadata(String name, Object value) {
        try {
            if (METADATA_HEADER_LAST_MODIFIED_DATE.equals(name) && !(value instanceof Date)) {
                value = ServiceUtils.parseIso8601Date(value.toString());
            } else if (METADATA_HEADER_DATE.equals(name) && !(value instanceof Date)) {
                value = ServiceUtils.parseIso8601Date(value.toString());
            }
        } catch (ParseException e) {
            if (log.isErrorEnabled()) {
                log.error("Unable to parse value we expect to be a valid date: " + name + "=" + value, e);
            }
        }

        super.addMetadata(name, value);
    }

    /**
     * Add all the metadata information to the object from the provided map.
     * 
     * @param metadata
     */
    public void addAllMetadata(Map metadata) {
        Iterator iter = metadata.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            addMetadata(entry.getKey().toString(), entry.getValue());
        }
    }

    /**
     * Returns only those object metadata items that can be modified in
     * S3. This list excludes those that are set by the S3 service, and
     * those that are specific to a particular HTTP request/response
     * session (such as request identifiers). 
     * 
     * @return
     * the limited set of metadata items that S3 allows users to control. 
     */
    public Map getModifiableMetadata() {
        Map objectMetadata = new HashMap(getMetadataMap());
        objectMetadata.remove(S3Object.METADATA_HEADER_CONTENT_LENGTH);
        objectMetadata.remove(S3Object.METADATA_HEADER_DATE);
        objectMetadata.remove(S3Object.METADATA_HEADER_ETAG);
        objectMetadata.remove(S3Object.METADATA_HEADER_LAST_MODIFIED_DATE);
        objectMetadata.remove(S3Object.METADATA_HEADER_OWNER);
        objectMetadata.remove("id-2"); // HTTP request-specific information
        objectMetadata.remove("request-id"); // HTTP request-specific information
        return objectMetadata;
    }

    public Object clone() {
        S3Object clone = new S3Object(key);
        clone.bucketName = bucketName;
        clone.dataInputStream = dataInputStream;
        clone.acl = acl;
        clone.isMetadataComplete = isMetadataComplete;
        clone.dataInputFile = dataInputFile;
        clone.addAllMetadata(getMetadataMap());
        return clone;
    }

    /**
     * Calculates the MD5 hash value of the given data object, and compares it
     * against this object's hash (as stored in the Content-MD5 header for 
     * uploads, or the ETag header for downloads). 
     * 
     * @param downloadedFile
     * @return
     * true if the calculated MD5 hash value of the file matches this object's 
     * hash value, false otherwise.
     * 
     * @throws NoSuchAlgorithmException
     * @throws FileNotFoundException
     * @throws IOException
     */
    public boolean verifyData(File downloadedFile)
            throws NoSuchAlgorithmException, FileNotFoundException, IOException {
        return getMd5HashAsBase64()
                .equals(ServiceUtils.toBase64(ServiceUtils.computeMD5Hash(new FileInputStream(downloadedFile))));
    }

    /**
     * Calculates the MD5 hash value of the given data object, and compares it
     * against this object's hash (as stored in the Content-MD5 header for 
     * uploads, or the ETag header for downloads). 
     * 
     * @param downloadedData
     * @return
     * true if the calculated MD5 hash value of the bytes matches this object's 
     * hash value, false otherwise.
     * 
     * @throws NoSuchAlgorithmException
     * @throws FileNotFoundException
     * @throws IOException
     */
    public boolean verifyData(byte[] downloadedData)
            throws NoSuchAlgorithmException, FileNotFoundException, IOException {
        return getMd5HashAsBase64().equals(ServiceUtils.toBase64(ServiceUtils.computeMD5Hash(downloadedData)));
    }

    /**
     * Calculates the MD5 hash value of the given data object, and compares it
     * against this object's hash (as stored in the Content-MD5 header for 
     * uploads, or the ETag header for downloads). 
     * 
     * @param downloadedDataStream
     * the input stream of a downloaded S3Object.
     * 
     * @return
     * true if the calculated MD5 hash value of the input stream matches this 
     * object's hash value, false otherwise.
     * 
     * @throws NoSuchAlgorithmException
     * @throws FileNotFoundException
     * @throws IOException
     */
    public boolean verifyData(InputStream downloadedDataStream)
            throws NoSuchAlgorithmException, FileNotFoundException, IOException {
        return getMd5HashAsBase64()
                .equals(ServiceUtils.toBase64(ServiceUtils.computeMD5Hash(downloadedDataStream)));
    }
}