com.clicktravel.infrastructure.persistence.aws.s3.S3FileStore.java Source code

Java tutorial

Introduction

Here is the source code for com.clicktravel.infrastructure.persistence.aws.s3.S3FileStore.java

Source

/*
 * Copyright 2014 Click Travel Ltd
 *
 * 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 com.clicktravel.infrastructure.persistence.aws.s3;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.*;
import com.clicktravel.cheddar.infrastructure.persistence.database.exception.NonExistentItemException;
import com.clicktravel.cheddar.infrastructure.persistence.exception.PersistenceResourceFailureException;
import com.clicktravel.cheddar.infrastructure.persistence.filestore.FileItem;
import com.clicktravel.cheddar.infrastructure.persistence.filestore.FilePath;
import com.clicktravel.cheddar.infrastructure.persistence.filestore.InternetFileStore;

public class S3FileStore implements InternetFileStore {

    private static final String USER_METADATA_HEADER_PREFIX = "x-amz-meta-";
    private static final String USER_METADATA_FILENAME = "filename";
    private static final String USER_METADATA_LAST_UPDATED_TIME = "last-updated-time";

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final String bucketSchema;
    private boolean initialized;
    private AmazonS3 amazonS3Client;
    private final DateTimeFormatter formatter = ISODateTimeFormat.dateTimeNoMillis().withZoneUTC();

    private final Collection<String> missingItemErrorCodes = Arrays.asList("NoSuchBucket", "NoSuchKey");

    public S3FileStore(final String bucketSchema) {
        this.bucketSchema = bucketSchema;
        initialized = false;
    }

    public void initialize(final AmazonS3 amazonS3Client) {
        this.amazonS3Client = amazonS3Client;
        initialized = true;
        logger.info("S3FileStore initialized.");
    }

    private void checkInitialization() {
        if (!initialized) {
            throw new IllegalStateException("S3FileStore not initialized.");
        }
    }

    @Override
    public FileItem read(final FilePath filePath) throws NonExistentItemException {
        checkInitialization();
        final GetObjectRequest getObjectRequest = new GetObjectRequest(bucketNameForFilePath(filePath),
                filePath.filename());
        try {
            final S3Object s3Object = amazonS3Client.getObject(getObjectRequest);
            final Map<String, String> userMetaData = getUserMetaData(s3Object);
            final String filename = userMetaData.get(USER_METADATA_FILENAME);
            final String lastUpdatedTimeStr = userMetaData.get(USER_METADATA_LAST_UPDATED_TIME);
            DateTime lastUpdatedTime = null;
            if (lastUpdatedTimeStr != null) {
                try {
                    lastUpdatedTime = formatter.parseDateTime(lastUpdatedTimeStr);
                } catch (final Exception e) {
                    logger.warn(e.getMessage(), e);
                }
            }
            try {

                final FileItem fileItem = new FileItem(filename, s3Object.getObjectContent(), lastUpdatedTime);
                return fileItem;
            } catch (final IOException e) {
                throw new IllegalStateException(e);
            }
        } catch (final AmazonS3Exception e) {
            if (missingItemErrorCodes.contains(e.getErrorCode())) {
                throw new NonExistentItemException(
                        "Item does not exist" + filePath.directory() + "->" + filePath.filename());
            }
            throw e;
        }
    }

    /**
     * Returns a Map of the user meta-data associated with the given S3 Object
     *
     * This is a work-around for a bug in the AWS Java SDK which treats HTTP headers in a case-insensitive manner.
     *
     * @see <a href="https://github.com/aws/aws-sdk-java/pull/326">https://github.com/aws/aws-sdk-java/pull/326</a>
     *
     * @param s3Object The S3Object for which the user meta-data is to be obtained.
     * @return key-value map for user meta-data
     */
    private Map<String, String> getUserMetaData(final S3Object s3Object) {
        final ObjectMetadata objectMetaData = s3Object.getObjectMetadata();
        final Map<String, String> userMetaData = objectMetaData.getUserMetadata();
        if (userMetaData.isEmpty()) {
            for (final Entry<String, Object> entry : objectMetaData.getRawMetadata().entrySet()) {
                final String normalisedKey = entry.getKey().toLowerCase();
                if (normalisedKey.startsWith(USER_METADATA_HEADER_PREFIX)) {
                    final String value = String.valueOf(entry.getValue());
                    userMetaData.put(normalisedKey.substring(USER_METADATA_HEADER_PREFIX.length()), value);
                }
            }
        }
        return userMetaData;
    }

    @Override
    public void write(final FilePath filePath, final FileItem fileItem) {
        checkInitialization();

        final ObjectMetadata metadata = new ObjectMetadata();
        metadata.addUserMetadata(USER_METADATA_FILENAME, fileItem.filename());
        metadata.addUserMetadata(USER_METADATA_LAST_UPDATED_TIME, formatter.print(fileItem.lastUpdatedTime()));
        metadata.setContentLength(fileItem.getBytes().length);
        final InputStream is = new ByteArrayInputStream(fileItem.getBytes());
        final String bucketName = bucketNameForFilePath(filePath);
        if (!amazonS3Client.doesBucketExist(bucketName)) {
            createBucket(bucketName);
        }
        final PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, filePath.filename(), is,
                metadata);
        amazonS3Client.putObject(putObjectRequest);
    }

    @Override
    public void delete(final FilePath filePath) {
        checkInitialization();
        try {
            amazonS3Client.deleteObject(bucketNameForFilePath(filePath), filePath.filename());
        } catch (final AmazonS3Exception e) {
            if (missingItemErrorCodes.contains(e.getErrorCode())) {
                throw new NonExistentItemException(
                        "Item does not exist" + filePath.directory() + "->" + filePath.filename());
            }
            throw e;
        }
    }

    private String bucketNameForFilePath(final FilePath filePath) {
        return bucketSchema + "-" + filePath.directory();
    }

    /**
     * Returns the correct bucket name for the supplied directory.
     *
     * @param directory The directory for which the bucket name is required.
     * @return The correct bucket name for the supplied directory.
     */
    private String bucketNameForDirectory(final String directory) {
        return bucketSchema + "-" + directory;
    }

    private void createBucket(final String bucketName) {
        amazonS3Client.createBucket(bucketName);
    }

    @Override
    public URL publicUrlForFilePath(final FilePath filePath) throws NonExistentItemException {
        return amazonS3Client.generatePresignedUrl(bucketNameForFilePath(filePath), filePath.filename(),
                DateTime.now().plusHours(1).toDate(), HttpMethod.GET);
    }

    @Override
    public List<FilePath> list(final String directory, final String prefix) {

        try {

            final List<FilePath> filePathList = new ArrayList<FilePath>();
            final ObjectListing objectListing = amazonS3Client.listObjects(bucketNameForDirectory(directory),
                    prefix);

            final List<S3ObjectSummary> s3objectSummaries = objectListing.getObjectSummaries();
            for (final S3ObjectSummary s3ObjectSummary : s3objectSummaries) {
                final FilePath filePath = new FilePath(directory, s3ObjectSummary.getKey());
                filePathList.add(filePath);
            }

            return filePathList;

        } catch (final AmazonS3Exception e) {
            throw new PersistenceResourceFailureException("An error occurred obtaining a listing of directory -> "
                    + directory + " with prefix -> " + prefix, e);
        }
    }
}