/*
 * Copyright 2013-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.services.s3;

import com.amazonaws.SdkClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.annotation.SdkInternalApi;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AnonymousAWSCredentials;
import com.amazonaws.internal.StaticCredentialsProvider;
import com.amazonaws.metrics.RequestMetricCollector;
import com.amazonaws.regions.Region;
import com.amazonaws.services.kms.AWSKMS;
import com.amazonaws.services.kms.AWSKMSClient;
import com.amazonaws.services.s3.internal.MultiFileOutputStream;
import com.amazonaws.services.s3.internal.PartCreationEvent;
import com.amazonaws.services.s3.internal.S3Direct;
import com.amazonaws.services.s3.internal.crypto.CryptoModuleDispatcher;
import com.amazonaws.services.s3.internal.crypto.S3CryptoModule;
import com.amazonaws.services.s3.model.AbortMultipartUploadRequest;
import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest;
import com.amazonaws.services.s3.model.CompleteMultipartUploadResult;
import com.amazonaws.services.s3.model.CopyPartRequest;
import com.amazonaws.services.s3.model.CopyPartResult;
import com.amazonaws.services.s3.model.CryptoConfiguration;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.EncryptedInitiateMultipartUploadRequest;
import com.amazonaws.services.s3.model.EncryptedPutObjectRequest;
import com.amazonaws.services.s3.model.EncryptionMaterials;
import com.amazonaws.services.s3.model.EncryptionMaterialsProvider;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.GroupGrantee;
import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest;
import com.amazonaws.services.s3.model.InitiateMultipartUploadResult;
import com.amazonaws.services.s3.model.InstructionFileId;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PartETag;
import com.amazonaws.services.s3.model.Permission;
import com.amazonaws.services.s3.model.PutInstructionFileRequest;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.PutObjectResult;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectId;
import com.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider;
import com.amazonaws.services.s3.model.UploadObjectRequest;
import com.amazonaws.services.s3.model.UploadPartRequest;
import com.amazonaws.services.s3.model.UploadPartResult;
import com.amazonaws.util.VersionInfoUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * Used to perform client-side encryption for storing data securely in S3. Data
 * encryption is done using a one-time randomly generated content encryption
 * key (CEK) per S3 object. 
 * <p>
 * The encryption materials specified in the constructor will be used to
 * protect the CEK which is then stored along side with the S3 object.
 */
public class AmazonS3EncryptionClient extends AmazonS3Client implements
        AmazonS3Encryption {
    public static final String USER_AGENT = AmazonS3EncryptionClient.class.getName()
            + "/" + VersionInfoUtils.getVersion();
    private final S3CryptoModule<?> crypto;
    private final AWSKMS kms;
    /**
     * True if the a default KMS client is constructed, which will be shut down
     * when this instance of S3 encryption client is shutdown.  False otherwise,
     * which means the users who provided the KMS client would be responsible
     * to shut down the KMS client. 
     */
    private final boolean isKMSClientInternal;

    // ///////////////////// Constructors ////////////////
    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client that will make <b>anonymous</b>
     * requests to Amazon S3.  If {@link #getObject(String, String)} is called,
     * the object contents will be decrypted with the encryption materials provided.
     * </p>
     * <p>
     * Only a subset of the Amazon S3 API will work with anonymous
     * <i>(i.e. unsigned)</i> requests, but this can prove useful in some situations.
     * For example:
     * <ul>
     *  <li>If an Amazon S3 bucket has {@link Permission#Read} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can call
     *  {@link #listObjects(String)} to see what objects are stored in a bucket.</li>
     *  <li>If an object has {@link Permission#Read} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can call
     *  {@link #getObject(String, String)} and
     *  {@link #getObjectMetadata(String, String)} to pull object content and
     *  metadata.</li>
     *  <li>If a bucket has {@link Permission#Write} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can upload objects
     *  to the bucket.</li>
     * </ul>
     * </p>
     *
     * @param encryptionMaterials
     *            The encryption materials to be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(EncryptionMaterials encryptionMaterials) {
        this(new StaticEncryptionMaterialsProvider(encryptionMaterials));
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client that will make <b>anonymous</b>
     * requests to Amazon S3.  If {@link #getObject(String, String)} is called,
     * the object contents will be decrypted with the encryption materials provided.
     * </p>
     * <p>
     * Only a subset of the Amazon S3 API will work with anonymous
     * <i>(i.e. unsigned)</i> requests, but this can prove useful in some situations.
     * For example:
     * <ul>
     *  <li>If an Amazon S3 bucket has {@link Permission#Read} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can call
     *  {@link #listObjects(String)} to see what objects are stored in a bucket.</li>
     *  <li>If an object has {@link Permission#Read} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can call
     *  {@link #getObject(String, String)} and
     *  {@link #getObjectMetadata(String, String)} to pull object content and
     *  metadata.</li>
     *  <li>If a bucket has {@link Permission#Write} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can upload objects
     *  to the bucket.</li>
     * </ul>
     * </p>
     *
     * @param encryptionMaterialsProvider
     *            A provider for the encryption materials to be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(
            EncryptionMaterialsProvider encryptionMaterialsProvider) {

        this(new StaticCredentialsProvider(new AnonymousAWSCredentials()),
                encryptionMaterialsProvider,
                configFactory.getConfig(), new CryptoConfiguration());
    }


    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client that will make <b>anonymous</b>
     * requests to Amazon S3.  If {@link #getObject(String, String)} is called,
     * the object contents will be decrypted with the encryption materials provided.
     * The encryption implementation of the provided crypto provider will be
     * used to encrypt and decrypt data.
     * </p>
     * <p>
     * Only a subset of the Amazon S3 API will work with anonymous
     * <i>(i.e. unsigned)</i> requests, but this can prove useful in some situations.
     * For example:
     * <ul>
     *  <li>If an Amazon S3 bucket has {@link Permission#Read} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can call
     *  {@link #listObjects(String)} to see what objects are stored in a bucket.</li>
     *  <li>If an object has {@link Permission#Read} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can call
     *  {@link #getObject(String, String)} and
     *  {@link #getObjectMetadata(String, String)} to pull object content and
     *  metadata.</li>
     *  <li>If a bucket has {@link Permission#Write} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can upload objects
     *  to the bucket.</li>
     * </ul>
     * </p>
     *
     * @param encryptionMaterials
     *              The encryption materials to be used to encrypt and decrypt data.
     * @param cryptoConfig
     *                The crypto configuration whose parameters will be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(EncryptionMaterials encryptionMaterials,
            CryptoConfiguration cryptoConfig) {
        this(new StaticEncryptionMaterialsProvider(encryptionMaterials),
                cryptoConfig);
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client that will make <b>anonymous</b>
     * requests to Amazon S3.  If {@link #getObject(String, String)} is called,
     * the object contents will be decrypted with the encryption materials provided.
     * The encryption implementation of the provided crypto provider will be
     * used to encrypt and decrypt data.
     * </p>
     * <p>
     * Only a subset of the Amazon S3 API will work with anonymous
     * <i>(i.e. unsigned)</i> requests, but this can prove useful in some situations.
     * For example:
     * <ul>
     *  <li>If an Amazon S3 bucket has {@link Permission#Read} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can call
     *  {@link #listObjects(String)} to see what objects are stored in a bucket.</li>
     *  <li>If an object has {@link Permission#Read} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can call
     *  {@link #getObject(String, String)} and
     *  {@link #getObjectMetadata(String, String)} to pull object content and
     *  metadata.</li>
     *  <li>If a bucket has {@link Permission#Write} permission for the
     *  {@link GroupGrantee#AllUsers} group, anonymous clients can upload objects
     *  to the bucket.</li>
     * </ul>
     * </p>
     *
     * @param encryptionMaterialsProvider
     *            A provider for the encryption materials to be used to encrypt and decrypt data.
     * @param cryptoConfig
     *            The crypto configuration whose parameters will be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(
            EncryptionMaterialsProvider encryptionMaterialsProvider,
            CryptoConfiguration cryptoConfig) {

        this(new StaticCredentialsProvider(new AnonymousAWSCredentials()),
                encryptionMaterialsProvider,
                configFactory.getConfig(), cryptoConfig);
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to
     * access Amazon S3.  Object contents will be encrypted and decrypted with the encryption
     * materials provided.
     * </p>
     *
     * @param credentials
     *            The AWS credentials to use when making requests to Amazon S3
     *            with this client.
     * @param encryptionMaterials
     *            The encryption materials to be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(AWSCredentials credentials,
            EncryptionMaterials encryptionMaterials) {
        this(credentials, new StaticEncryptionMaterialsProvider(
                encryptionMaterials));
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to
     * access Amazon S3.  Object contents will be encrypted and decrypted with the encryption
     * materials provided.
     * </p>
     *
     * @param credentials
     *            The AWS credentials to use when making requests to Amazon S3
     *            with this client.
     * @param encryptionMaterialsProvider
     *            A provider for the encryption materials to be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(AWSCredentials credentials,
            EncryptionMaterialsProvider encryptionMaterialsProvider) {
        this(credentials, encryptionMaterialsProvider,
                configFactory.getConfig(), new CryptoConfiguration());
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to
     * access Amazon S3.  Object contents will be encrypted and decrypted with the encryption
     * materials provided.
     * </p>
     *
     * @param credentialsProvider
     *            The AWS credentials provider which will provide credentials
     *            to authenticate requests with AWS services.
     * @param encryptionMaterialsProvider
     *            A provider for the encryption materials to be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(
            AWSCredentialsProvider credentialsProvider,
            EncryptionMaterialsProvider encryptionMaterialsProvider) {
        this(credentialsProvider, encryptionMaterialsProvider,
                configFactory.getConfig(), new CryptoConfiguration());
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to
     * access Amazon S3.  Object contents will be encrypted and decrypted with the encryption
     * materials provided.  The encryption implementation of the provided crypto provider will
     * be used to encrypt and decrypt data.
     * </p>
     *
     * @param credentials
     *            The AWS credentials to use when making requests to Amazon S3
     *            with this client.
     * @param encryptionMaterials
     *            The encryption materials to be used to encrypt and decrypt data.
     * @param cryptoConfig
     *            The crypto configuration whose parameters will be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(AWSCredentials credentials,
            EncryptionMaterials encryptionMaterials,
            CryptoConfiguration cryptoConfig) {
        this(credentials, new StaticEncryptionMaterialsProvider(
                encryptionMaterials), cryptoConfig);
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to
     * access Amazon S3.  Object contents will be encrypted and decrypted with the encryption
     * materials provided.  The encryption implementation of the provided crypto provider will
     * be used to encrypt and decrypt data.
     * </p>
     *
     * @param credentials
     *            The AWS credentials to use when making requests to Amazon S3
     *            with this client.
     * @param encryptionMaterialsProvider
     *            A provider for the encryption materials to be used to encrypt and decrypt data.
     * @param cryptoConfig
     *            The crypto configuration whose parameters will be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(AWSCredentials credentials,
            EncryptionMaterialsProvider encryptionMaterialsProvider,
            CryptoConfiguration cryptoConfig) {
        this(credentials, encryptionMaterialsProvider,
                configFactory.getConfig(), cryptoConfig);
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to
     * access Amazon S3.  Object contents will be encrypted and decrypted with the encryption
     * materials provided.  The encryption implementation of the provided crypto provider will
     * be used to encrypt and decrypt data.
     * </p>
     *
     * @param credentialsProvider
     *            The AWS credentials provider which will provide credentials
     *            to authenticate requests with AWS services.
     * @param encryptionMaterialsProvider
     *            A provider for the encryption materials to be used to encrypt and decrypt data.
     * @param cryptoConfig
     *            The crypto configuration whose parameters will be used to encrypt and decrypt data.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(
            AWSCredentialsProvider credentialsProvider,
            EncryptionMaterialsProvider encryptionMaterialsProvider,
            CryptoConfiguration cryptoConfig) {
        this(credentialsProvider, encryptionMaterialsProvider,
                configFactory.getConfig(), cryptoConfig);
    }

    /**
     * <p>
     * Constructs a new Amazon S3 Encryption client using the specified AWS credentials and
     * client configuration to access Amazon S3.  Object contents will be encrypted and decrypted
     * with the encryption materials provided. The crypto provider and storage mode denoted in
     * the specified crypto configuration will be used to encrypt and decrypt data.
     * </p>
     *
     * @param credentials
     *            The AWS credentials to use when making requests to Amazon S3
     *            with this client.
     * @param encryptionMaterials
     *            The encryption materials to be used to encrypt and decrypt data.
     * @param clientConfig
     *            The client configuration options controlling how this client
     *            connects to Amazon S3 (ex: proxy settings, retry counts, etc).
     * @param cryptoConfig
     *            The crypto configuration whose parameters will be used to encrypt and decrypt data.
     * @throws IllegalArgumentException
     *            If either of the encryption materials or crypto configuration parameters are null.
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withClientConfiguration(ClientConfiguration)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(AWSCredentials credentials,
            EncryptionMaterials encryptionMaterials,
            ClientConfiguration clientConfig, CryptoConfiguration cryptoConfig) {
        this(credentials, new StaticEncryptionMaterialsProvider(
                encryptionMaterials), clientConfig, cryptoConfig);
    }

    /**
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withClientConfiguration(ClientConfiguration)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(AWSCredentials credentials,
            EncryptionMaterialsProvider encryptionMaterialsProvider,
            ClientConfiguration clientConfig, CryptoConfiguration cryptoConfig) {
        this(new StaticCredentialsProvider(credentials),
                encryptionMaterialsProvider, clientConfig, cryptoConfig);
    }

    /**
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withClientConfiguration(ClientConfiguration)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(
            AWSCredentialsProvider credentialsProvider,
            EncryptionMaterialsProvider kekMaterialsProvider,
            ClientConfiguration clientConfig,
            CryptoConfiguration cryptoConfig) {
        this(credentialsProvider, kekMaterialsProvider, clientConfig,
                cryptoConfig,
                null    // request metric collector
        );
    }

    /**
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withClientConfiguration(ClientConfiguration)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withMetricsCollector(RequestMetricCollector)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(
            AWSCredentialsProvider credentialsProvider,
            EncryptionMaterialsProvider kekMaterialsProvider,
            ClientConfiguration clientConfig,
            CryptoConfiguration cryptoConfig,
            RequestMetricCollector requestMetricCollector) {
        this(null, // KMS client
            credentialsProvider, kekMaterialsProvider, clientConfig,
            cryptoConfig, requestMetricCollector);
    }

    /**
     * @deprecated use {@link AmazonS3EncryptionClientBuilder#withEncryptionMaterials(EncryptionMaterialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCredentials(AWSCredentialsProvider)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withCryptoConfiguration(CryptoConfiguration)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withClientConfiguration(ClientConfiguration)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withMetricsCollector(RequestMetricCollector)} and
     *                 {@link AmazonS3EncryptionClientBuilder#withKmsClient(AWSKMS)}
     */
    @Deprecated
    public AmazonS3EncryptionClient(AWSKMSClient kms,
            AWSCredentialsProvider credentialsProvider,
            EncryptionMaterialsProvider kekMaterialsProvider,
            ClientConfiguration clientConfig,
            CryptoConfiguration cryptoConfig,
            RequestMetricCollector requestMetricCollector) {
        super(credentialsProvider, clientConfig, requestMetricCollector);
        assertParameterNotNull(kekMaterialsProvider,
                "EncryptionMaterialsProvider parameter must not be null.");
        assertParameterNotNull(cryptoConfig,
                "CryptoConfiguration parameter must not be null.");
        this.isKMSClientInternal = kms == null;
        this.kms = isKMSClientInternal 
            ? newAWSKMSClient(credentialsProvider, clientConfig, cryptoConfig, 
                    requestMetricCollector)
            : kms;
        this.crypto = new CryptoModuleDispatcher(this.kms, new S3DirectImpl(),
                credentialsProvider, kekMaterialsProvider, cryptoConfig);
    }

    @SdkInternalApi
    AmazonS3EncryptionClient(AmazonS3EncryptionClientParams params) {
        super(params);
        assertParameterNotNull(params.getEncryptionMaterials(), "EncryptionMaterialsProvider parameter must not be null.");
        assertParameterNotNull(params.getCryptoConfiguration(), "CryptoConfiguration parameter must not be null.");
        this.isKMSClientInternal = params.getKmsClient() == null;
        this.kms = isKMSClientInternal ?
                newAWSKMSClient(params.getClientParams().getCredentialsProvider(),
                        params.getClientParams().getClientConfiguration(),
                        params.getCryptoConfiguration(),
                        params.getClientParams().getRequestMetricCollector()) : params.getKmsClient();
        this.crypto = new CryptoModuleDispatcher(this.kms, new S3DirectImpl(),
                params.getClientParams().getCredentialsProvider(), params.getEncryptionMaterials(), params.getCryptoConfiguration());
    }

    public static AmazonS3EncryptionClientBuilder encryptionBuilder() {
        return AmazonS3EncryptionClientBuilder.standard();
    }

    /**
     * Creates and returns a new instance of AWS KMS client in the case when
     * an explicit AWS KMS client is not specified.
     */
    private AWSKMSClient newAWSKMSClient(
            AWSCredentialsProvider credentialsProvider,
            ClientConfiguration clientConfig,
            CryptoConfiguration cryptoConfig,
            RequestMetricCollector requestMetricCollector
    ) {
        final AWSKMSClient kmsClient = new AWSKMSClient(
            credentialsProvider, clientConfig, requestMetricCollector);
        final Region kmsRegion = cryptoConfig.getAwsKmsRegion();
        if (kmsRegion != null)
            kmsClient.setRegion(kmsRegion);
        return kmsClient;
    }

    private void assertParameterNotNull(Object parameterValue,
            String errorMessage) {
        if (parameterValue == null)
            throw new IllegalArgumentException(errorMessage);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Use {@link EncryptedPutObjectRequest} to specify materialsDescription for the EncryptionMaterials to be used for 
     * this request.AmazonS3EncryptionClient would use {@link EncryptionMaterialsProvider#getEncryptionMaterials(java.util.Map)} to 
     * retrieve encryption materials corresponding to the materialsDescription specified in the current request.
     * </p>
     * 
     */
    @Override
    public PutObjectResult putObject(PutObjectRequest req) {
        return crypto.putObjectSecurely(req.clone());
    }

    @Override
    public S3Object getObject(GetObjectRequest req) {
        return crypto.getObjectSecurely(req);
    }

    @Override
    public ObjectMetadata getObject(GetObjectRequest req, File dest) {
        return crypto.getObjectSecurely(req, dest);
    }

    @Override
    public void deleteObject(DeleteObjectRequest req) {
        req.getRequestClientOptions().appendUserAgent(USER_AGENT);
        // Delete the object
        super.deleteObject(req);
        // If it exists, delete the instruction file.
        InstructionFileId ifid = new S3ObjectId(req.getBucketName(), req.getKey()).instructionFileId();

        DeleteObjectRequest instructionDeleteRequest = (DeleteObjectRequest) req.clone();
        instructionDeleteRequest.withBucketName(ifid.getBucket()).withKey(ifid.getKey());
        super.deleteObject(instructionDeleteRequest);
    }

    @Override
    public CompleteMultipartUploadResult completeMultipartUpload(
            CompleteMultipartUploadRequest req) {
        return crypto.completeMultipartUploadSecurely(req);
    }

    /** 
     * {@inheritDoc}
     * <p>
     * Use {@link EncryptedInitiateMultipartUploadRequest} to specify materialsDescription for the EncryptionMaterials to be used for this request.
     * AmazonS3EncryptionClient would use {@link EncryptionMaterialsProvider#getEncryptionMaterials(java.util.Map)} to retrieve encryption materials
     * corresponding to the materialsDescription specified in the current request.
     * </p>
     */
    @Override
    public InitiateMultipartUploadResult initiateMultipartUpload(
            InitiateMultipartUploadRequest req) {
        boolean isCreateEncryptionMaterial = true;
        if (req instanceof EncryptedInitiateMultipartUploadRequest) {
            EncryptedInitiateMultipartUploadRequest cryptoReq = 
                    (EncryptedInitiateMultipartUploadRequest) req;
            isCreateEncryptionMaterial = cryptoReq.isCreateEncryptionMaterial();
        }
        return isCreateEncryptionMaterial
             ? crypto.initiateMultipartUploadSecurely(req)
             : super.initiateMultipartUpload(req)
             ;
    }

    /**
     * {@inheritDoc}
     *
     * <p>
     * <b>NOTE:</b> Because the encryption process requires context from block
     * N-1 in order to encrypt block N, parts uploaded with the
     * AmazonS3EncryptionClient (as opposed to the normal AmazonS3Client) must
     * be uploaded serially, and in order. Otherwise, the previous encryption
     * context isn't available to use when encrypting the current part.
     */
    @Override
    public UploadPartResult uploadPart(UploadPartRequest uploadPartRequest)
            throws SdkClientException, AmazonServiceException {
        return crypto.uploadPartSecurely(uploadPartRequest);
    }

    @Override
    public CopyPartResult copyPart(CopyPartRequest copyPartRequest) {
        return crypto.copyPartSecurely(copyPartRequest);
    }

    @Override
    public void abortMultipartUpload(AbortMultipartUploadRequest req) {
        crypto.abortMultipartUploadSecurely(req);
    }

    /**
     * Creates a new crypto instruction file by re-encrypting the CEK of an
     * existing encrypted S3 object with a new encryption material identifiable
     * via a new set of material description.
     *<p> 
     * User of this method is responsible for explicitly deleting/updating the
     * instruction file so created should the corresponding S3 object is
     * deleted/created.
     * 
     * @return the result of the put (instruction file) operation.
     */
    public PutObjectResult putInstructionFile(PutInstructionFileRequest req) {
        return crypto.putInstructionFileSecurely(req);
    }

    /**
     * {@inheritDoc}
     * <p>
     * If the a default internal KMS client has been constructed, it will also be
     * shut down by calling this method.
     * Otherwise, users who provided the KMS client would be responsible to
     * shut down the KMS client extrinsic to this method.
     */
    @Override
    public void shutdown() {
        super.shutdown();
        if (isKMSClientInternal)
            kms.shutdown();
    }

    // /////////////////// Access to the methods in the super class //////////
    /**
     * An internal implementation used to provide limited but direct access to
     * the underlying methods of AmazonS3Client without any encryption or
     * decryption operations.
     */
    private final class S3DirectImpl extends S3Direct {
        @Override
        public PutObjectResult putObject(PutObjectRequest req) {
            return AmazonS3EncryptionClient.super.putObject(req);
        }

        @Override
        public S3Object getObject(GetObjectRequest req) {
            return AmazonS3EncryptionClient.super.getObject(req);
        }

        @Override
        public ObjectMetadata getObject(GetObjectRequest req, File dest) {
            return AmazonS3EncryptionClient.super.getObject(req, dest);
        }

        @Override
        public CompleteMultipartUploadResult completeMultipartUpload(
                CompleteMultipartUploadRequest req) {
            return AmazonS3EncryptionClient.super.completeMultipartUpload(req);
        }

        @Override
        public InitiateMultipartUploadResult initiateMultipartUpload(
                InitiateMultipartUploadRequest req) {
            return AmazonS3EncryptionClient.super.initiateMultipartUpload(req);
        }

        @Override
        public UploadPartResult uploadPart(UploadPartRequest req)
                throws SdkClientException, AmazonServiceException {
            return AmazonS3EncryptionClient.super.uploadPart(req);
        }

        @Override
        public CopyPartResult copyPart(CopyPartRequest req) {
            return AmazonS3EncryptionClient.super.copyPart(req);
        }

        @Override
        public void abortMultipartUpload(AbortMultipartUploadRequest req) {
            AmazonS3EncryptionClient.super.abortMultipartUpload(req);
        }
    }

    /**
     * Used to encrypt data first to disk with pipelined concurrent multi-part
     * uploads to S3. This method enables significant speed-up of encrypting and
     * uploading large payloads to Amazon S3 via pipelining and parallel uploads
     * by consuming temporary disk space.
     * <p>
     * There are many ways you can customize the behavior of this method,
     * including
     * <ul>
     * <li>the configuration of your own custom thread pool</li>
     * <li>the part size of each multi-part upload request; By default, a
     * temporary ciphertext file is generated per part and gets uploaded
     * immediately to S3</li>
     * <li>the maximum temporary disk space that must not be exceeded by
     * execution of this request; By default, the encryption will block upon
     * hitting the limit and will only resume when the in-flight uploads catch
     * up by releasing the temporary disk space upon successful uploads of the
     * completed parts</li>
     * <li>the configuration of your own {@link MultiFileOutputStream} for
     * custom pipeline behavior</li>
     * <li>the configuration of your own {@link UploadObjectObserver} for custom
     * multi-part upload behavior</li>
     * </ul>
     * <p>
     * A request is handled with the following life cycle, calling the necessary
     * Service Provider Interface:
     * <ol>
     * <li>A thread pool is constructed (or retrieved from the request) for the
     * execution of concurrent upload tasks to be submitted by the
     * <code>UploadObjectObserver</code></li>
     * <li>An {@link UploadObjectObserver} is constructed (or retrieved from the
     * request) for execution of concurrent uploads to S3</li>
     * <li>Initialize the <code>UploadObjectObserver</code></li>
     * <li>Initialize a multi-part upload request to S3 by calling
     * {@link UploadObjectObserver#onUploadInitiation(UploadObjectRequest)}</li>
     * <li>A {@link MultiFileOutputStream} is constructed (or retrieved from the
     * request) which serves as the pipeline for incremental (but serial)
     * encryption to disk with concurrent multipart uploads to S3 whenever the
     * parts on the disk are ready</li>
     * <li>Initialize the <code>MultiFileOutputStream</code></li>
     * <li>Kicks off the pipeline for incremental encryption to disk with
     * pipelined concurrent multi-part uploads to S3</li>
     * <li>For every part encrypted into a temporary file on disk, it is
     * uploaded by calling
     * {@link UploadObjectObserver#onPartCreate(PartCreationEvent)}</li>
     * <li>Finally, clean up and complete the multi-part upload by calling
     * {@link UploadObjectObserver#onCompletion(List)}.</li>
     * </ol>
     * 
     * @return the result of the completed muti-part uploads
     * 
     * @throws IOException
     *             if the encryption to disk failed
     * @throws InterruptedException
     *             if the current thread was interrupted while waiting
     * @throws ExecutionException
     *             if the concurrent uploads threw an exception
     */
    public CompleteMultipartUploadResult uploadObject(final UploadObjectRequest req)
            throws IOException, InterruptedException, ExecutionException {
        // Set up the pipeline for concurrent encrypt and upload
        // Set up a thread pool for this pipeline
        ExecutorService es = req.getExecutorService();
        final boolean defaultExecutorService = es == null;
        if (es == null)
            es = Executors.newFixedThreadPool(clientConfiguration.getMaxConnections());
        UploadObjectObserver observer = req.getUploadObjectObserver();
        if (observer == null)
            observer = new UploadObjectObserver();
        // initialize the observer
        observer.init(req, new S3DirectImpl(), this, es);
        // Initiate upload
        final String uploadId = observer.onUploadInitiation(req);
        final List<PartETag> partETags = new ArrayList<PartETag>();
        MultiFileOutputStream mfos = req.getMultiFileOutputStream();
        if (mfos == null)
            mfos = new MultiFileOutputStream();
        try {
            // initialize the multi-file output stream
            mfos.init(observer, req.getPartSize(), req.getDiskLimit());
            // Kicks off the encryption-upload pipeline;
            // Note mfos is automatically closed upon method completion.
            crypto.putLocalObjectSecurely(req, uploadId, mfos);
            // block till all part have been uploaded
            for (Future<UploadPartResult> future: observer.getFutures()) {
                UploadPartResult partResult = future.get();
                partETags.add(new PartETag(partResult.getPartNumber(), partResult.getETag()));
            }
        } catch(IOException ex) {
            throw onAbort(observer, ex);
        } catch(InterruptedException ex) {
            throw onAbort(observer, ex);
        } catch(ExecutionException ex) {
            throw onAbort(observer, ex);
        } catch(RuntimeException ex) {
            throw onAbort(observer, ex);
        } catch(Error ex) {
            throw onAbort(observer, ex);
        } finally {
            if (defaultExecutorService)
                es.shutdownNow();   // shut down the locally created thread pool
            mfos.cleanup();       // delete left-over temp files
        }
        // Complete upload
        return observer.onCompletion(partETags);
    }

    /**
     * Convenient method to notifies the observer to abort the multi-part
     * upload, and returns the original exception.
     */
    private <T extends Throwable> T onAbort(UploadObjectObserver observer, T t) {
        observer.onAbort();
        return t;
    }
}