package com.nifty.cloud.ncs.service;

import com.nifty.cloud.ncs.*;
import com.nifty.cloud.ncs.auth.Credentials;
import com.nifty.cloud.ncs.http.ClientProperties;
import com.nifty.cloud.ncs.http.HttpMethod;
import com.nifty.cloud.ncs.service.acl.Policy;
import com.nifty.cloud.ncs.service.parser.*;
import com.nifty.cloud.ncs.services.validator.BucketNameValidator;
import com.nifty.cloud.ncs.services.validator.ObjectNameValidator;
import com.nifty.cloud.ncs.services.validator.ValidationMessage;
import com.nifty.cloud.ncs.util.DateUtils;
import com.nifty.cloud.ncs.util.XmlWriter;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import uk.co.lucasweb.aws.v4.signer.HttpRequest;
import uk.co.lucasweb.aws.v4.signer.Signer;
import uk.co.lucasweb.aws.v4.signer.credentials.AwsCredentials;
import uk.co.lucasweb.aws.v4.signer.encoding.URLEncoding;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.Map.Entry;

/**
 * ニフクラストレージクライアントです。
 */
public class StorageClient extends NiftyCloudClient {
    private static final String SERVICE_NAME = "NIFCLOUD Storage Service";
    private static final String STORAGE_HOSTNAME = "jp-east-1.storage.api.nifcloud.com";

    private static Log log = LogFactory.getLog(StorageClient.class);

    private Credentials credentials;

    private static final String SEPARATOR = ", ";
    private static final int DEFAULT_STREAM_BUFFER_SIZE = 128 * 1024;

    private static final int SC_NOT_MODIFIED = 304;
    private static final int SC_PRECONDITION_FAILED = 412;

    private static final String EMPTY_STRING_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

    private boolean isNifty = false;
    private static final String NOT_NIFTY_HEADER_PREFIX = "x-amz-";
    private static final String NOT_NIFTY_AUTHORIZATION_STRING = "AWS";

    private static final String REGION_FOR_SIGNER = "jp-east-3";

    /**
     * 指定したアクセスキー、シークレットキーを使用してStorageClientを構築します。
     *
     * @param credentials アクセスキー、シークレットキー情報
     */
    public StorageClient(Credentials credentials) {
        this(credentials, new ClientProperties());
    }

    /**
     * 指定したアクセスキー、シークレットキー、およびクライアント設定を使用してStorageClientを構築します。
     *
     * @param credentials アクセスキー、シークレットキー情報
     * @param properties  ストレージクライアントの設定
     */
    public StorageClient(Credentials credentials, ClientProperties properties) {
        super(properties);
        this.credentials = credentials;
        this.setEndpoint(STORAGE_HOSTNAME);

        isNifty = false;
        credentials.setAuthorizationHeaderPrefix(NOT_NIFTY_AUTHORIZATION_STRING);
        credentials.setExtendedHeaderPrefix(NOT_NIFTY_HEADER_PREFIX);
        StorageHeadersImpl.setPrefix(NOT_NIFTY_HEADER_PREFIX);
    }

    /**
     * ニフクラサービスのエンドポイントを設定します。
     *
     * @param endpoint ニフクラサービスのエンドポイント
     * @throws IllegalArgumentException
     */
    public void setEndpoint(String endpoint) throws IllegalArgumentException {
        super.setEndpoint(endpoint);

        isNifty = false;
        credentials.setAuthorizationHeaderPrefix(NOT_NIFTY_AUTHORIZATION_STRING);
        credentials.setExtendedHeaderPrefix(NOT_NIFTY_HEADER_PREFIX);
        StorageHeadersImpl.setPrefix(NOT_NIFTY_HEADER_PREFIX);

    }

    /**
     * バケット一覧を取得します。
     *
     * @return NcsBucketのリスト
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public List<NcsBucket> getService() throws NiftyCloudClientException, NiftyCloudServiceException {
        // 要求生成
        NiftyCloudRequest request = createRequest(null, null, HttpMethod.GET);

        // 認証文字列生成
        addSignV4(request);

        // 実行
        List<NcsBucket> bucketList = execute(request, new GetServiceParser(),
                new StorageErrorResponseParser());

        return (bucketList);
    }

    /**
     * 指定した名前のバケットを作成します。
     *
     * @param bucketName バケット名
     * @return 作成されたNcsBucket
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public NcsBucket putBucket(String bucketName)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (putBucket(bucketName, null));
    }

    /**
     * 指定したバケットを作成します。
     *
     * @param bucket バケット
     * @return 作成されたNcsBucket
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public NcsBucket putBucket(NcsBucket bucket)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (putBucket(bucket, null));
    }

    /**
     * バケット名とACLを指定してバケットを作成します。
     *
     * @param bucketName バケット名
     * @param policy     設定したいACL(privateのみ指定可能)
     * @return 作成されたNcsBucket
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public NcsBucket putBucket(String bucketName, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        // バケット名の妥当性確認
        bucketName = validateBucketName(bucketName);

        // 要求生成
        NiftyCloudRequest request = createRequest(bucketName, null,
                HttpMethod.PUT);

        // 認証文字列生成
        addSignV4(request);

        // 実行
        execute(request, new StorageResponseParser(),
                new StorageErrorResponseParser());

        return (new NcsBucket(bucketName));
    }

    /**
     * バケット情報とACLを指定してバケットを作成します。
     *
     * @param bucket バケット情報
     * @param policy 設定したいACL(privateのみ指定可能)
     * @return 作成されたNcsBucket
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public NcsBucket putBucket(NcsBucket bucket, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        String bucketName = bucket.getName();
        // バケット名の妥当性確認
        bucketName = validateBucketName(bucketName);

        // 要求生成
        NiftyCloudRequest request = createRequest(bucketName, null,
                HttpMethod.PUT);

        // 認証文字列生成
        addSignV4(request);

        // 実行
        execute(request, new StorageResponseParser(),
                new StorageErrorResponseParser());

        return (new NcsBucket(bucketName));
    }

    /**
     * 指定したバケットを削除します。
     *
     * @param bucketName 削除したいバケット名
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public void deleteBucket(String bucketName) throws NiftyCloudClientException, NiftyCloudServiceException {
        // バケット名の妥当性確認
        bucketName = validateBucketName(bucketName);

        // 要求生成
        NiftyCloudRequest request = createRequest(bucketName, null, HttpMethod.DELETE);

        // 認証文字列生成
        addSignV4(request);

        // 実行
        execute(request, new StorageResponseParser(), new StorageErrorResponseParser());
    }

    /**
     * 指定バケット内のオブジェクト一覧を取得します。
     *
     * @param bucketName オブジェクト一覧取得対象のバケット名
     * @param prefix     prefixパラメータ
     * @param marker     markerパラメータ
     * @param maxKeys    max-keysパラメータ
     * @return 指定バケット内のオブジェクト一覧
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public ObjectListing listObjects(String bucketName, String prefix, String marker, Integer maxKeys)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        // バケット名の妥当性確認
        bucketName = validateBucketName(bucketName);

        // 要求生成
        NiftyCloudRequest request = createRequest(bucketName, null, HttpMethod.GET);

        // パラメータ追加
        if (prefix != null) {
            request.addParameter("prefix", prefix);
        }
        if (marker != null) {
            request.addParameter("marker", marker);
        }
        if (maxKeys != null && maxKeys.intValue() > 0) {
            request.addParameter("max-keys", maxKeys.toString());
        }

        // 認証文字列生成
        addSignV4(request);

        // 実行
        ObjectListing listObjects = execute(request, new ListObjectsParser(), new StorageErrorResponseParser());

        return (listObjects);
    }

    /**
     * 指定バケット内のオブジェクト一覧を取得します。
     *
     * @param bucketName オブジェクト一覧取得対象のバケット名
     * @param delimiter  delimiterパラメータ
     * @param prefix     prefixパラメータ
     * @param marker     markerパラメータ
     * @param maxKeys    max-keysパラメータ
     * @return 指定バケット内のオブジェクト一覧
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public ObjectListing listObjects(String bucketName, String delimiter, String prefix, String marker, Integer maxKeys)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        // バケット名の妥当性確認
        bucketName = validateBucketName(bucketName);

        // 要求生成
        NiftyCloudRequest request = createRequest(bucketName, null, HttpMethod.GET);

        // パラメータ追加
        if (delimiter != null) {
            if (delimiter.equals("/") == false) {
                throw (new NiftyCloudClientException("The delimiter \"" + delimiter + "\" is not allowed"));
            }
            request.addParameter("delimiter", delimiter);
        }
        if (prefix != null) {
            request.addParameter("prefix", prefix);
        }
        if (marker != null) {
            request.addParameter("marker", marker);
        }
        if (maxKeys != null && maxKeys.intValue() > 0) {
            request.addParameter("max-keys", maxKeys.toString());
        }

        // 認証文字列生成
        addSignV4(request);

        // 実行
        ObjectListing listObjects = execute(request, new ListObjectsParser(), new StorageErrorResponseParser());

        return (listObjects);
    }

    /**
     * 指定したオブジェクトのメタデータを返却します。
     *
     * @param bucketName 取得対象オブジェクトを格納するバケット名
     * @param objectName 取得対象オブジェクト名
     * @return 取得したMetadataを保持するHashMap
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public Map<String, Object> headObject(String bucketName, String objectName)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        // バケット名妥当性確認
        bucketName = validateBucketName(bucketName);

        // オブジェクト名妥当性確認
        objectName = validateObjectName(objectName);

        // 要求生成
        NiftyCloudRequest request = createRequest(bucketName, objectName, HttpMethod.HEAD);

        // 認証用文字列生成
        addSignV4(request, objectName);

        try {
            // 実行
            NcsObject object = execute(request, new ObjectResponseParser(), new StorageErrorResponseParser());

            if (object != null) {
                return object.getMetadata();
            } else {
                return (null);
            }

        } catch (StorageException e) {
            /*
             * If the request failed because one of the specified constraints
             * was not met (ex: mathing ETag, modified since date, etc.), then
             * return null, so that users don't have to wrap their code in
             * try/catch blocks and check for this status code if they want to
             * use constraints.
             */
            if (e.getStatusCode() == SC_PRECONDITION_FAILED || e.getStatusCode() == SC_NOT_MODIFIED) {
                return (null);
            }

            throw (e);
        }
    }

    /**
     * 指定したオブジェクトをメモリ上に取得します。
     *
     * @param bucketName 取得対象オブジェクトを格納するバケット名
     * @param objectName 取得対象オブジェクト名
     * @param range      Rangeヘッダに設定する値
     * @return 取得したNcsObject
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public NcsObject getObject(String bucketName, String objectName, long[] range)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (getObject(new NcsObject(bucketName, objectName), range));
    }

    /**
     * 指定したオブジェクトを取得し、ファイルに保存します。
     *
     * @param bucketName 取得対象オブジェクトを格納するバケット名
     * @param objectName 取得対象オブジェクト名
     * @param range      Rangeヘッダに設定される値
     * @param fileName   保存対象ファイル名
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public void getObject(String bucketName, String objectName, long[] range, String fileName)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        NcsObject object = getObject(bucketName, objectName, range);

        if (object == null) {
            return;
        }

        // ファイル出力
        outputObjectToFile(object, fileName);
    }

    /**
     * NcsObjectにより指定したオブジェクトを取得し、ファイルに保存します。
     *
     * @param object   取得するオブジェクト情報を含むNcsObject
     * @param fileName 保存対象ファイル名
     */
    public void getObject(NcsObject object, String fileName)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        NcsObject responseObject = getObject(object, (long[]) null);

        if (responseObject == null) {
            return;
        }

        // ファイル出力
        outputObjectToFile(responseObject, fileName);
    }

    public void getObjectPart(String bucketName, String objectName, long contentLength, long[] range, String fileName)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        NcsObject object = getObject(bucketName, objectName, range);

        if (object == null) {
            return;
        }

        // ファイル出力
        outputObjectPartToFile(object, contentLength, range[0], fileName);
    }

    /**
     * ファイル名を指定してオブジェクトを作成します。
     *
     * @param bucketName 作成対象オブジェクトが格納されるバケット名
     * @param objectName 作成対象オブジェクト名
     * @param fileName   アップロードするファイル名
     * @param policy     設定するACL(privateのみ指定可能)
     * @return 作成したオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String putObject(String bucketName, String objectName, String fileName, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        if (fileName == null) {
            throw (new NiftyCloudClientException("The argument of fileName must be specified."));
        }

        return (putObject(bucketName, objectName, new File(fileName), policy));
    }

    /**
     * ファイルを指定してオブジェクトを作成します。
     *
     * @param bucketName 作成対象オブジェクトが格納されるバケット名
     * @param objectName 作成対象オブジェクト名
     * @param file       アップロードするファイル
     * @param policy     設定するACL(privateのみ指定可能)
     * @return 作成したオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String putObject(String bucketName, String objectName, File file, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        NcsObject object = new NcsObject();
        object.setBucketName(bucketName);
        object.setObjectName(objectName);

        return (putObject(object, file, policy));
    }

    /**
     * NcsObjectおよびファイルを指定してオブジェクトを作成します。
     *
     * @param object 作成するオブジェクト情報を含むNcsObject
     * @param file   アップロードするファイル
     * @param policy 設定するACL(privateのみ指定可能)
     * @return 作成したオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String putObject(NcsObject object, File file, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        if (object == null) {
            throw (new NiftyCloudClientException("NcsObject must not be null."));
        }

        if (file == null) {
            throw (new NiftyCloudClientException("The argument of file must be specified."));
        }

        InputStream input = null;
        // Mimetypeの取得
        String contentType = Mimetypes.getInstance().getMimetype(file);
        if (object.getContentType() == null) {
            object.setContentType(contentType);
        }

        // ディレクトリ
        if (file.isDirectory()) {
            return (putObject(object, policy));
        }

        // Content-Lengthの設定
        object.setContentLength(file.length());

        String contentSHA256 = "";
        try {
            FileInputStream streamForHash = new FileInputStream(file);
            byte[] sha256Hash = computeSHA256Hash(streamForHash);
            streamForHash.close();
            contentSHA256 = Hex.encodeHexString(sha256Hash);
        } catch (Exception e) {
            throw (new NiftyCloudClientException("Can't calculate SHA256 hash: " + e.getMessage(), e));
        }

        // Content-MD5の計算
        try {
            byte[] md5Hash = computeMD5Hash(new FileInputStream(file));
            object.setContentMD5(toBase64(md5Hash));
        } catch (Exception e) {
            throw (new NiftyCloudClientException("Can't calculate MD5 hash: " + e.getMessage(), e));
        }

        try {
            input = new NiftyCloudFileInputStream(file);
        } catch (FileNotFoundException e) {
            throw (new NiftyCloudClientException("Can't find file to upload.", e));
        }

        return (putObject(object, input, contentSHA256, null, policy));
    }

    /**
     * 入力ストリームを指定してオブジェクトを作成します。
     *
     * @param bucketName    作成対象オブジェクトが格納されるバケット名
     * @param objectName    作成対象オブジェクト
     * @param input         オブジェクトのボディとなる入力ストリーム
     * @param contentSHA256 オブジェクトのボディのSHA256ハッシュ値
     * @param contentLength データサイズ
     * @param contentType   コンテンツタイプ
     * @param policy        設定するACL(privateのみ指定可能)
     * @return 作成したオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String putObject(String bucketName, String objectName, InputStream input, String contentSHA256, long contentLength, String contentType, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        NcsObject object = new NcsObject();
        object.setBucketName(bucketName);
        object.setObjectName(objectName);
        object.setContentLength(contentLength);

        return (putObject(object, input, contentSHA256, contentType, policy));
    }

    /**
     * NcsObjectおよび入力ストリームを指定してオブジェクトを作成します。
     *
     * @param object        作成するオブジェクトの情報を含むNcsObject
     * @param input         オブジェクトのボディとなる入力ストリーム
     * @param contentSHA256 オブジェクトのボディのSHA256ハッシュ値
     * @param contentType   コンテンツタイプ
     * @param policy        設定するACL(privateのみ指定可能)
     * @return 作成したオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String putObject(NcsObject object, InputStream input, String contentSHA256, String contentType, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        if (object == null) {
            throw (new NiftyCloudClientException("NcsObject must not be null."));
        }

        if (input == null) {
            throw (new NiftyCloudClientException("InputStream must not be null."));
        }

        if (contentSHA256 == null) {
            throw (new NiftyCloudClientException("contentSHA256 must not be null."));
        }

        // バケット名妥当性確認
        String bucketName = validateBucketName(object.getBucketName());
        object.setBucketName(bucketName);

        // オブジェクト名妥当性確認
        String objectName = validateObjectName(object.getObjectName());
        object.setObjectName(objectName);

        if (contentType != null) {
            object.setContentType(contentType);
        }

        // 要求生成
        NiftyCloudRequest request = createRequest(object.getBucketName(), object.getObjectName(),
                HttpMethod.PUT);

        if (object.getContentLength() < 0) {
            throw (new NiftyCloudClientException("Negative value content length specified for stream data."));
        }

        if (input.markSupported() == false) {
            input = new NiftyCloudInputStream(input, DEFAULT_STREAM_BUFFER_SIZE);
        }

        if (object.getContentMD5() == null) {
            try {
                input = new NiftyCloudMD5InputStream(input);
            } catch (NoSuchAlgorithmException e) {
                log.warn("No MD5 digest algorithm available.", e);
            }
        }

        // Content-Typeの設定
        if (object.getContentType() == null) {
            object.setContentType(Mimetypes.MIMETYPE_OCTET_STREAM);
        }

        setMetadata(request, object);

        // 認証文字列生成
        addSignV4(request, object.getObjectName(), contentSHA256);

        request.setContent(input);

        NcsObject returnedObject = null;

        try {
            // 実行
            returnedObject = execute(request, new ObjectResponseParser(),
                    new StorageErrorResponseParser());
        } finally {
            try {
                input.close();
            } catch (IOException e) {
                log.error("Can't cleanly close input stream: " + e.getMessage(), e);
            }
        }

        String contentMD5 = object.getContentMD5();
        if (input instanceof NiftyCloudMD5InputStream) {
            NiftyCloudMD5InputStream md5InputStream = (NiftyCloudMD5InputStream) input;
            contentMD5 = toBase64(md5InputStream.getMd5Digest());
        }

        if (returnedObject != null && contentMD5 != null) {
            byte[] clientHash = fromBase64(contentMD5);
            byte[] serverHash = fromHex(returnedObject.getETag());

            if (Arrays.equals(clientHash, serverHash) == false) {
                throw (new NiftyCloudClientException("Can't verify integrity of data upload. "
                        + "Content hash don't match. "
                        + "You may need to delete the data stored in NIFCLOUD Storage."));
            }
        }

        closeInputStream(returnedObject.getContent());

        return (returnedObject.getETag());
    }

    /**
     * バケット名、オブジェクト名を指定してディレクトリを作成します。
     *
     * @param bucketName バケット名
     * @param objectName オブジェクト名
     * @param policy     設定するACL(privateのみ指定可能)
     * @return 作成したオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String putObject(String bucketName, String objectName, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (putObject(new NcsObject(bucketName, objectName), policy));
    }

    /**
     * NcsObjectを指定してディレクトリを作成します。
     *
     * @param object 作成するオブジェクトの情報を含むNcsObject
     * @param policy 設定するACL(privateのみ指定可能)
     * @return 作成したオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String putObject(NcsObject object, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        if (object == null) {
            throw (new NiftyCloudClientException("NcsObject must not be null."));
        }

        // バケット名妥当性確認
        String bucketName = validateBucketName(object.getBucketName());
        object.setBucketName(bucketName);

        // オブジェクト名妥当性確認
        String objectName = validateObjectName(object.getObjectName());
        object.setObjectName(objectName);

        // 要求生成
        NiftyCloudRequest request = createRequest(object.getBucketName(), object.getObjectName(),
                HttpMethod.PUT);

        if (object.getContentLength() < 0) {
            throw (new NiftyCloudClientException("Negative value content length specified for stream data."));
        }

        // Content-Typeの設定
        object.setContentType(Mimetypes.MIMETYPE_DIRECTORY);

        setMetadata(request, object);

        // ディレクトリ
        request.removeHeader("Content-MD5");

        // 認証文字列生成
        addSignV4(request, object.getObjectName());

        NcsObject returnedObject = null;

        // 実行
        returnedObject = execute(request, new ObjectResponseParser(),
                new StorageErrorResponseParser());

        closeInputStream(returnedObject.getContent());

        return (returnedObject.getETag());
    }

    /**
     * オブジェクトをコピーします。
     *
     * @param srcBucketName  コピー元バケット名
     * @param srcObjectName  コピー元オブジェクト名
     * @param destBucketName 宛先バケット名
     * @param destObjectName 宛先オブジェクト名
     * @return コピーしたオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String copyObject(String srcBucketName, String srcObjectName, String destBucketName, String destObjectName)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (copyObject(srcBucketName, srcObjectName, destBucketName, destObjectName, null));
    }

    /**
     * オブジェクトをコピーします。
     *
     * @param srcBucketName  コピー元バケット名
     * @param srcObjectName  コピー元オブジェクト名
     * @param destBucketName 宛先バケット名
     * @param destObjectName 宛先オブジェクト名
     * @param policy         コピーするオブジェクトのACL(privateのみ指定可能)
     * @return コピーしたオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String copyObject(String srcBucketName, String srcObjectName,
                             String destBucketName, String destObjectName, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (copyObject(new NcsObject(srcBucketName, srcObjectName),
                new NcsObject(destBucketName, destObjectName), policy));
    }

    /**
     * オブジェクトをコピーします。
     *
     * @param srcObject  コピー元オブジェクトの情報を含むNcsObject
     * @param destObject 宛先オブジェクトの情報を含むNcsObject<br>
     *                   メタデータを設定するには{@link NcsObject#addUserMetadata(String, String)}メソッドを使用します。
     * @return コピーしたオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String copyObject(NcsObject srcObject, NcsObject destObject)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (copyObject(srcObject, destObject, null));
    }

    /**
     * オブジェクトをコピーします。
     *
     * @param srcObject  コピー元オブジェクトの情報を含むNcsObject
     * @param destObject 宛先オブジェクトの情報を含むNcsObject<br>
     *                   メタデータを設定するには{@link NcsObject#addUserMetadata(String, String)}メソッドを使用します。
     * @param policy     コピーするオブジェクトのACL(privateのみ指定可能)
     * @return コピーしたオブジェクトのMD5ハッシュ値
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String copyObject(NcsObject srcObject, NcsObject destObject, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        if (srcObject == null || destObject == null) {
            throw (new NiftyCloudClientException("NcsObject must not be null."));
        }

        // バケット名妥当性確認
        String srcBucketName = validateBucketName(srcObject.getBucketName());
        String destBucketName = validateBucketName(destObject.getBucketName());

        // オブジェクト名妥当性確認
        String srcObjectName = validateObjectName(srcObject.getObjectName());
        String destObjectName = validateObjectName(destObject.getObjectName());

        // リクエスト生成
        NiftyCloudRequest request = createRequest(destBucketName, destObjectName, HttpMethod.PUT);
        request.addHeader(StorageHeadersImpl.COPY_SOURCE, String.format("/%s/%s", srcBucketName, urlEncode(srcObjectName)));

        // メタデータ設定
        setMetadata(request, destObject);

        // 認証文字列生成
        addSignV4(request, destObjectName);

        CopyObjectResult result = execute(request, new CopyObjectParser(),
                new StorageErrorResponseParser());

        if (result.getException() != null) {
            throw (result.getException());
        }

        return (result.getETag());
    }

    /**
     * 指定したオブジェクトを削除します。
     *
     * @param bucketName 削除対象オブジェクトが格納されているバケット名
     * @param objectName 削除対象オブジェクト名
     */
    public void deleteObject(String bucketName, String objectName)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        // バケット名妥当性確認
        bucketName = validateBucketName(bucketName);

        // オブジェクト名妥当性確認
        objectName = validateObjectName(objectName);

        // 要求生成
        NiftyCloudRequest request = createRequest(bucketName, objectName, HttpMethod.DELETE);

        // 認証文字列生成
        addSignV4(request, objectName);

        // 実行
        execute(request, new StorageResponseParser(), new StorageErrorResponseParser());
    }

    /**
     * バケット名、オブジェクト名を指定して、マルチパートアップロードの準備をします。
     *
     * @param bucketName バケット名
     * @param objectName オブジェクト名
     * @return マルチパートアップロードの識別ID
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String initiateMultipartUpload(String bucketName, String objectName)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (initiateMultipartUpload(bucketName, objectName, null));
    }

    /**
     * バケット名、オブジェクト名、ACLを指定して、マルチパートアップロードの準備をします。
     *
     * @param bucketName バケット名
     * @param objectName オブジェクト名
     * @param policy     設定するACL(privateのみ指定可能)
     * @return uploadId
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String initiateMultipartUpload(String bucketName, String objectName, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        return (initiateMultipartUpload(new NcsObject(bucketName, objectName), policy));
    }

    /**
     * NcsObject、ACLを指定して、マルチパートアップロードの準備をします。
     *
     * @param object 作成するオブジェクト情報を含むNcsObject
     * @param policy 設定するACL(privateのみ指定可能)
     * @return マルチパートアップロードの識別ID
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public String initiateMultipartUpload(NcsObject object, Policy policy)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        if (object == null) {
            throw (new NiftyCloudClientException("NcsObject must not be null."));
        }

        // バケット名妥当性確認
        String bucketName = validateBucketName(object.getBucketName());

        // オブジェクト名妥当性確認
        String objectName = validateObjectName(object.getObjectName());

        // リクエスト生成
        NiftyCloudRequest request = createRequest(bucketName, objectName, HttpMethod.POST);

        request.addParameter("uploads", null);
        request.setContent(new ByteArrayInputStream(new byte[0]));
        object.setContentLength(0);

        // メタデータ設定
        setMetadata(request, object);

        // 認証文字列生成
        addSignV4(request, objectName);

        // 実行
        InitiateMultipartUploadResult returnedObject =
                execute(request, new InitiateMultipartUploadParser(), new StorageErrorResponseParser());

        return (returnedObject.getUploadId());
    }

    /**
     * マルチパートアップロードのパートをアップロードします。
     *
     * @param uploadPartRequest アップロードするパート情報を含むUploadPartRequest
     * @return パートデータのETag
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public PartETag uploadPart(UploadPartRequest uploadPartRequest)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        if (uploadPartRequest == null) {
            throw (new IllegalArgumentException("UploadPartRequest can not be null."));
        }

        // バケット名妥当性確認
        String bucketName = validateBucketName(uploadPartRequest.getBucketName());

        // オブジェクト名妥当性確認
        String objectName = validateObjectName(uploadPartRequest.getObjectName());

        String uploadId = uploadPartRequest.getUploadId();
        int partNumber = uploadPartRequest.getPartNumber();
        int partSize = uploadPartRequest.getPartSize();

        if (uploadId == null) {
            throw (new NiftyCloudClientException("UploadId can not be null."));
        }

        // リクエスト生成
        NiftyCloudRequest request = createRequest(bucketName, objectName, HttpMethod.PUT);
        request.addParameter("uploadId", uploadId);
        request.addParameter("partNumber", String.valueOf(partNumber));

        if (uploadPartRequest.getMd5Digest() != null) {
            request.addHeader(StorageHeadersImpl.CONTENT_MD5, uploadPartRequest.getMd5Digest());
        }

        request.addHeader(StorageHeadersImpl.CONTENT_LENGTH, String.valueOf(partSize));
        String contentSHA256 = "";
        InputStream inputStream = null;
        if (uploadPartRequest.getInputStream() != null) {
            inputStream = uploadPartRequest.getInputStream();
            contentSHA256 = uploadPartRequest.getContentSHA256();
        } else if (uploadPartRequest.getFile() != null) {
            try {
                InputStream streamForHash = new NiftyCloudInputSubstream(new NiftyCloudFileInputStream(
                        uploadPartRequest.getFile()),
                        uploadPartRequest.getFileOffset(), partSize, true);
                byte[] sha256Hash = computeSHA256Hash(streamForHash);
                streamForHash.close();
                contentSHA256 = Hex.encodeHexString(sha256Hash);
            } catch (Exception e) {
                throw (new NiftyCloudClientException(
                        "Can't generate sha256 digest", e));
            }

            try {
                inputStream = new NiftyCloudInputSubstream(new NiftyCloudFileInputStream(
                        uploadPartRequest.getFile()),
                        uploadPartRequest.getFileOffset(), partSize, true);
            } catch (FileNotFoundException e) {
                throw (new NiftyCloudClientException(
                        "The specified file doesn't exist", e));
            }
        } else {
            throw (new NiftyCloudClientException(
                    "A File or InputStream must be specified when uploading part"));
        }

        NiftyCloudMD5InputStream md5DigestStream = null;
        if (uploadPartRequest.getMd5Digest() == null) {
            try {
                md5DigestStream = new NiftyCloudMD5InputStream(inputStream);
                inputStream = md5DigestStream;
            } catch (NoSuchAlgorithmException e) {
                log.warn("No MD5 digest algorithm available.", e);
            }
        }

        try {
            request.setContent(inputStream);

            // 認証文字列生成
            addSignV4(request, objectName, contentSHA256);

            // 実行
            NcsObject returnedObject =
                    execute(request, new ObjectResponseParser(), new StorageErrorResponseParser());

            if (returnedObject != null && md5DigestStream != null) {
                String contentMd5 = toBase64(md5DigestStream.getMd5Digest());
                byte[] clientSideHash = fromBase64(contentMd5);
                byte[] serverSideHash = fromHex(returnedObject.getETag());

                if (!Arrays.equals(clientSideHash, serverSideHash)) {
                    throw new NiftyCloudClientException(
                            "Can't verify integrity of data upload. " +
                                    "Content hash don't match. " +
                                    "You may need to delete the data stored in NIFCLOUD Storage.");
                }
            }

            PartETag partETag = null;
            if (returnedObject != null) {
                partETag = new PartETag(partNumber, returnedObject.getETag());
            }

            closeInputStream(returnedObject.getContent());

            return (partETag);

        } catch (NiftyCloudClientException e) {
            throw (e);

        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (Exception e) {
                    // NOP
                }
            }
        }
    }

    /**
     * マルチパートアップロードをキャンセルします。
     *
     * @param bucketName バケット名
     * @param objectName オブジェクト名
     * @param uploadId   マルチパートアップロードの識別ID
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public void abortMultipartUpload(String bucketName, String objectName, String uploadId)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        // バケット名妥当性確認
        bucketName = validateBucketName(bucketName);

        // オブジェクト名妥当性確認
        objectName = validateObjectName(objectName);

        if (uploadId == null) {
            throw (new IllegalArgumentException("UploadId can not be null."));
        }

        // リクエスト生成
        NiftyCloudRequest request = createRequest(bucketName, objectName, HttpMethod.DELETE);
        request.addParameter("uploadId", uploadId);

        // 認証文字列生成
        addSignV4(request, objectName);

        // 実行
        execute(request, new StorageResponseParser(), new StorageErrorResponseParser());
    }

    /**
     * マルチパートアップロードを完了します。
     *
     * @param bucketName バケット名
     * @param objectName オブジェクト名
     * @param uploadId   マルチパートアップロードの識別ID
     * @param partETags  パートデータ群のETagリスト
     * @return CompleteMultipartUploadの結果
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    public CompleteMultipartUploadResult completeMultipartUpload(String bucketName, String objectName, String uploadId, List<PartETag> partETags)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        // バケット名妥当性確認
        bucketName = validateBucketName(bucketName);

        // オブジェクト名妥当性確認
        objectName = validateObjectName(objectName);

        if (uploadId == null) {
            throw (new IllegalArgumentException("UploadId can not be null."));
        }
        if (partETags == null) {
            throw (new IllegalArgumentException("PartETags can not be null."));
        }

        // リクエスト生成
        NiftyCloudRequest request = createRequest(bucketName, objectName, HttpMethod.POST);
        request.addParameter("uploadId", uploadId);
        byte[] eTagListContent = createETagListContent(partETags);
        request.setContent(new ByteArrayInputStream(eTagListContent));
        request.addHeader(StorageHeadersImpl.CONTENT_LENGTH, String.valueOf(eTagListContent.length));

        String contentSHA256 = "";
        try {
            ByteArrayInputStream streamForHash = new ByteArrayInputStream(eTagListContent);
            byte[] sha256Hash = computeSHA256Hash(streamForHash);
            streamForHash.close();
            contentSHA256 = Hex.encodeHexString(sha256Hash);
        } catch (Exception e) {
            throw (new NiftyCloudClientException("Can't calculate SHA256 hash: " + e.getMessage(), e));
        }

        // 認証文字列生成
        addSignV4(request, objectName, contentSHA256);

        CompleteMultipartUploadResult returnedObject =
                execute(request, new CompleteMultipartUploadParser(), new StorageErrorResponseParser());

        return (returnedObject);
    }

    /**
     * NcsObjectにより指定したオブジェクトをメモリ上に取得します。
     *
     * @param object
     * @param range
     * @return
     * @throws NiftyCloudClientException
     * @throws NiftyCloudServiceException
     */
    protected NcsObject getObject(NcsObject object, long[] range)
            throws NiftyCloudClientException, NiftyCloudServiceException {
        if (object == null) {
            throw (new NiftyCloudClientException("NcsObject must not be null."));
        }

        // バケット名妥当性確認
        String bucketName = validateBucketName(object.getBucketName());

        // オブジェクト名妥当性確認
        String objectName = validateObjectName(object.getObjectName());

        // 要求生成
        NiftyCloudRequest request = createRequest(bucketName, objectName, HttpMethod.GET);

        // Range設定
        if (range != null) {
            request.addHeader(StorageHeadersImpl.RANGE, String.format("bytes=%d-%d", range[0], range[1]));
        }

        setMetadata(request, object);

        // 認証用文字列生成
        addSignV4(request, objectName);

        try {
            // 実行
            NcsObject responseObject = execute(request, new ObjectResponseParser(),
                    new StorageErrorResponseParser());
            responseObject.setBucketName(bucketName);
            responseObject.setObjectName(objectName);

            return (responseObject);

        } catch (StorageException e) {
            /*
             * If the request failed because one of the specified constraints
             * was not met (ex: mathing ETag, modified since date, etc.), then
             * return null, so that users don't have to wrap their code in
             * try/catch blocks and check for this status code if they want to
             * use constraints.
             */
            if (e.getStatusCode() == SC_PRECONDITION_FAILED || e.getStatusCode() == SC_NOT_MODIFIED) {
                return (null);
            }

            throw (e);
        }
    }

    /**
     * 認証文字列を生成します。
     *
     * @param bucketName
     * @param objectName
     * @param method
     * @return NiftyCloudRequest
     */
    private NiftyCloudRequest createRequest(String bucketName,
                                            String objectName, HttpMethod method) {
        NiftyCloudRequest request = new NiftyCloudRequest(SERVICE_NAME, method);

        if (bucketName != null) {
            request.setEndpoint(getPathStyleEndPoint(bucketName));
            if (objectName != null) {
                request.setResourcePath(URLEncoding.encodePath(objectName));
            }
        } else {
            // GetService用
            request.setEndpoint(endpoint);
        }

        return (request);
    }

    /**
     * バケット名の妥当性を確認します。
     *
     * @param bucketName バケット名
     * @return bucketName
     * バケット名（trim済み）
     * @throws NiftyCloudClientException The bucket name is invalid.
     */
    private String validateBucketName(String bucketName)
            throws NiftyCloudClientException {
        List<ValidationMessage> messages = BucketNameValidator.validate(bucketName);
        if (messages.size() > 0) {
            throw (new NiftyCloudClientException(errorMessages(messages)));
        }

        return (bucketName.trim());
    }

    /**
     * オブジェクト名の妥当性を確認します。
     *
     * @param objectName オブジェクト名
     * @return オブジェクト名（trim済み）
     * @throws NiftyCloudClientException The object name is invalid.
     */
    private String validateObjectName(String objectName)
            throws NiftyCloudClientException {
        List<ValidationMessage> messages = ObjectNameValidator.validate(objectName);
        if (messages.size() > 0) {
            throw (new NiftyCloudClientException(errorMessages(messages)));
        }

        return (objectName.trim());
    }

    private String errorMessages(List<ValidationMessage> messages) {
        StringBuffer sb = new StringBuffer();

        String sep = "";
        for (ValidationMessage message : messages) {
            sb.append(sep);
            sb.append("Message: ");
            sb.append(message.getMessage());
            sb.append(", ");
            sb.append("Values: ");
            sb.append(message.getValue());
            sep = SEPARATOR;
        }
        return (sb.toString());
    }

    private URI getPathStyleEndPoint(String bucketName) {
        String uri = String.format("%s://%s/%s", endpoint.getScheme(), endpoint.getAuthority(), bucketName);

        try {
            return (new URI(uri));
        } catch (URISyntaxException e) {
            throw (new NiftyCloudClientException("Can't convert URI: " + uri));
        }
    }

    private void addSignV4(NiftyCloudRequest request) {
        addSignV4(request, null, EMPTY_STRING_SHA256);
    }

    private void addSignV4(NiftyCloudRequest request, String rawResourcePath) {
        addSignV4(request, rawResourcePath, EMPTY_STRING_SHA256);
    }

    private void addSignV4(NiftyCloudRequest request, String rawResourcePath, String contentSHA256) {
        // SignerにはURLエンコード前のパスを渡さないといけない　
        String encodedResourcePath = null;
        if (rawResourcePath != null) {
            encodedResourcePath = request.getResourcePath();
            request.setResourcePath(rawResourcePath);
        }

        String currentAmzDateTime = getCurrentAmzDateTime();
        request.addHeader(StorageHeadersImpl.NIFTY_PREFIX + StorageHeadersImpl.DATE, currentAmzDateTime);
        request.addHeader(StorageHeadersImpl.NIFTY_PREFIX + StorageHeadersImpl.CONTENT_SHA256, contentSHA256);

        HttpRequest requestForSigner = getHttpRequestForSigner(request);
        Signer.Builder signerBuilder = Signer.builder()
                .region(REGION_FOR_SIGNER)
                .awsCredentials(new AwsCredentials(credentials.getAccessKey(), credentials.getSecretKey()))
                .header("host", getHostHeaderValue(request));

        Map<String, String> headers = request.getHeaders();
        for (Entry<String, String> header : headers.entrySet()) {
            signerBuilder.header(header.getKey(), header.getValue());
        }

        String signature = signerBuilder
                .buildS3(requestForSigner, contentSHA256)
                .getSignature();
        request.addHeader("Authorization", signature);

        if (encodedResourcePath != null) {
            request.setResourcePath(encodedResourcePath);
        }
    }

    private uk.co.lucasweb.aws.v4.signer.HttpRequest getHttpRequestForSigner(NiftyCloudRequest request) {
        String pathAndQuery = getPathAndQuery(request);
        return new uk.co.lucasweb.aws.v4.signer.HttpRequest(request.getHttpMethod().toString(), pathAndQuery);
    }

    private String getCurrentAmzDateTime() {
        return DateUtils.formatAmzDate(new Date());
    }

    private String getHostHeaderValue(NiftyCloudRequest request) {
        StringBuilder hostHeader = new StringBuilder();
        hostHeader.append(request.getEndPoint().getHost());
        if (request.isUsingDefaultPort() == false) {
            hostHeader.append(":").append(endpoint.getPort());
        }

        return hostHeader.toString();
    }

    /**
     * @param request
     * @param object
     */
    private void setMetadata(NiftyCloudRequest request, NcsObject object) {
        Map<String, Object> rawMetadata = object.getRawMetadata();
        if (rawMetadata != null) {
            for (Entry<String, Object> entry : rawMetadata.entrySet()) {
                request.addHeader(entry.getKey(), entry.getValue().toString());
            }
        }

        Map<String, String> userMetadata = object.getUserMetadata();
        if (userMetadata != null) {
            for (Entry<String, String> entry : userMetadata.entrySet()) {
                request.addHeader(StorageHeadersImpl.NIFTY_USER_METADATA_PREFIX + entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * @param request
     * @param header
     * @param value
     */
    private static void addDateHeader(NiftyCloudRequest request, String header, Date value) {
        if (value != null) {
            request.addHeader(header, DateUtils.formatRfc822Date(value));
        }
    }

    /**
     * @param request
     * @param header
     * @param values
     */
    protected static void addStringListHeader(NiftyCloudRequest request, String header, List<String> values) {
        if ((values != null) && (values.isEmpty() == false)) {
            request.addHeader(header, join(values));
        }
    }

    /**
     * @param values
     * @return
     */
    private static String join(List<String> values) {
        StringBuffer sb = new StringBuffer();
        String sep = "";

        for (String value : values) {
            sb.append(sep);
            sb.append(value);
            sep = SEPARATOR;
        }

        return (sb.toString());
    }

    /**
     * @param is
     * @return
     * @throws NoSuchAlgorithmException
     * @throws IOException
     */
    private byte[] computeMD5Hash(InputStream is) throws NoSuchAlgorithmException, IOException {
        BufferedInputStream bis = new BufferedInputStream(is);

        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] bytes = new byte[16 * 1024];
            int readBytes = -1;
            while ((readBytes = bis.read(bytes, 0, bytes.length)) != -1) {
                messageDigest.update(bytes, 0, readBytes);
            }
            return (messageDigest.digest());
        } finally {
            try {
                bis.close();
            } catch (Exception e) {
                log.error("Can't close input stream of hash candidate: ", e);
            }
        }
    }

    protected byte[] computeSHA256Hash(InputStream is) throws NoSuchAlgorithmException, IOException {
        BufferedInputStream bis = new BufferedInputStream(is);

        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            byte[] bytes = new byte[16 * 1024];
            int readBytes = -1;
            while ((readBytes = bis.read(bytes, 0, bytes.length)) != -1) {
                messageDigest.update(bytes, 0, readBytes);
            }
            return (messageDigest.digest());
        } finally {
            try {
                bis.close();
            } catch (Exception e) {
                log.error("Can't close input stream of hash candidate: ", e);
            }
        }
    }

    /**
     * @param value
     * @return
     */
    private String toBase64(byte[] value) {
        byte[] base64 = Base64.encodeBase64(value);

        return (new String(base64));
    }

    /**
     * @param value
     * @return
     */
    private byte[] fromBase64(String value) {
        byte[] data;

        try {
            data = Base64.decodeBase64(value.getBytes(DEFAULT_ENCODING));
        } catch (UnsupportedEncodingException e) {
            log.warn("Tried to Base64-decode a String with the wrong encoding: ", e);
            data = Base64.decodeBase64(value.getBytes());
        }

        return (data);
    }

    /**
     * @param value
     * @return
     */
    private byte[] fromHex(String value) {
        byte[] data = new byte[(value.length() + 1) / 2];
        String hex = null;
        int strOffset = 0;
        int byteOffset = 0;

        while (strOffset < value.length()) {
            hex = value.substring(strOffset, strOffset + 2);
            strOffset += 2;
            data[byteOffset++] = (byte) Integer.parseInt(hex, 16);
        }

        return (data);
    }

    protected void outputObjectToFile(NcsObject object, String fileName) {
        if (fileName == null) {
            throw (new NiftyCloudClientException("Local download file name is null."));
        }

        BufferedOutputStream bos = null;
        BufferedInputStream bis = null;
        File file = new File(fileName);

        try {
            bos = new BufferedOutputStream(new FileOutputStream(file), DEFAULT_STREAM_BUFFER_SIZE);
            bis = new BufferedInputStream(object.getContent());

            byte[] bytes = new byte[DEFAULT_STREAM_BUFFER_SIZE];
            int length = 0;
            while ((length = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, length);
            }
        } catch (Exception e) {
            throw (new NiftyCloudClientException(e.getMessage(), e));
        } finally {
            try {
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                log.error("Can't cleanly close output stream: " + e.getMessage(), e);
            } finally {
                try {
                    if (bis != null) {
                        bis.close();
                    } else if (object.getContent() != null) {
                        object.getContent().close();
                    }
                } catch (IOException e) {
                    log.error("Can't cleanly close input stream: " + e.getMessage(), e);
                }
            }
        }
    }

    protected void outputObjectPartToFile(NcsObject object, long contentLength, long offset, String fileName) {
        if (fileName == null) {
            throw (new NiftyCloudClientException("Local download file name is null."));
        }

        RandomAccessFile raf = null;
        BufferedInputStream bis = null;

        try {
            raf = new RandomAccessFile(fileName, "rw");
            bis = new BufferedInputStream(object.getContent());

            byte[] bytes = new byte[DEFAULT_STREAM_BUFFER_SIZE];
            int length = 0;
            // 開始位置までシーク
            raf.setLength(contentLength);
            raf.seek(offset);
            while ((length = bis.read(bytes)) != -1) {
                raf.write(bytes, 0, length);
            }
        } catch (Exception e) {
            throw (new NiftyCloudClientException(e.getMessage(), e));
        } finally {
            try {
                if (raf != null) {
                    raf.close();
                }
            } catch (IOException e) {
                log.error("Can't cleanly close output stream: " + e.getMessage(), e);
            } finally {
                try {
                    if (bis != null) {
                        bis.close();
                    } else if (object.getContent() != null) {
                        object.getContent().close();
                    }
                } catch (IOException e) {
                    log.error("Can't cleanly close input stream: " + e.getMessage(), e);
                }
            }
        }
    }

    /**
     * 入力ストリームを閉じます。
     *
     * @param input
     */
    private void closeInputStream(InputStream input) {
        try {
            if (input != null) {
                input.close();
            }
        } catch (IOException e) {
            log.error("Can't cleanly close input stream: " + e.getMessage(), e);
        }
    }

    /**
     * @param partETags
     * @return
     */
    private byte[] createETagListContent(List<PartETag> partETags) {
        if (partETags == null) return null;

        XmlWriter xml = new XmlWriter();
        xml.start("CompleteMultipartUpload");
        List<PartETag> sortedPartETags = new ArrayList<PartETag>(partETags);
        Collections.sort(sortedPartETags, new Comparator<PartETag>() {
            public int compare(PartETag tag1, PartETag tag2) {
                if (tag1.getPartNumber() < tag2.getPartNumber()) {
                    return (-1);
                }
                if (tag1.getPartNumber() > tag2.getPartNumber()) {
                    return (1);
                }
                return (0);
            }
        });

        for (PartETag partETag : sortedPartETags) {
            xml.start("Part");
            xml.start("PartNumber").value(Integer.toString(partETag.getPartNumber())).end();
            xml.start("ETag").value(partETag.getETag()).end();
            xml.end();    // Part
        }
        xml.end();    // CompleteMultipartUpload

        return (xml.getBytes());
    }
}
