/**
 * Copyright Microsoft Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.microsoft.azure.storage.core;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map.Entry;

import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.StorageCredentials;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.StorageUri;

/**
 * RESERVED FOR INTERNAL USE. A class to help modify paths
 */
public final class PathUtility {
    /**
     * Adds a queryString to an URI.
     *
     * @param resourceURI
     *            the URI of the resource
     * @param fieldCollection
     *            the key/ values collection to append.
     * @return an appended URI.
     * @throws URISyntaxException
     *             if the resulting URI is invalid.
     * @throws StorageException
     */
    public static URI addToSingleUriQuery(final URI resourceURI, final HashMap<String, String[]> fieldCollection)
            throws URISyntaxException, StorageException {
        if (resourceURI == null) {
            return null;
        }

        final UriQueryBuilder outUri = new UriQueryBuilder();

        // Generate new queryString
        for (final Entry<String, String[]> entry : fieldCollection.entrySet()) {
            for (final String val : entry.getValue()) {
                outUri.add(entry.getKey(), val);
            }
        }

        return outUri.addToURI(resourceURI);
    }

    /**
     * Adds a queryString to an URI.
     *
     * @param resourceURI
     *            the URI of the resource
     * @param queryString
     *            the query string to add
     * @return an appended URI.
     * @throws URISyntaxException
     *             if the resulting URI is invalid.
     * @throws StorageException
     */
    public static StorageUri addToQuery(final StorageUri resourceURI, final String queryString)
            throws URISyntaxException, StorageException {
        return new StorageUri(addToSingleUriQuery(resourceURI.getPrimaryUri(), parseQueryString(queryString)),
                addToSingleUriQuery(resourceURI.getSecondaryUri(), parseQueryString(queryString)));
    }

    /**
     * Adds a queryString to an URI.
     *
     * @param resourceURI
     *            the URI of the resource
     * @param queryString
     *            the query string to add
     * @return an appended URI.
     * @throws URISyntaxException
     *             if the resulting URI is invalid.
     * @throws StorageException
     */
    public static URI addToQuery(final URI resourceURI, final String queryString) throws URISyntaxException,
            StorageException {
        return addToSingleUriQuery(resourceURI, parseQueryString(queryString));
    }

    /**
     * Appends a path to a list of URIs correctly using "/" as separator.
     *
     * @param uriList
     *            The base Uri.
     * @param relativeOrAbslouteUri
     *            The relative or absloute URI.
     * @return The appended Uri.
     * @throws URISyntaxException
     */
    public static StorageUri appendPathToUri(final StorageUri uriList, final String relativeOrAbsoluteUri)
            throws URISyntaxException {
        return appendPathToUri(uriList, relativeOrAbsoluteUri, "/");
    }

    /**
     * Appends a path to a list of URIs correctly using "/" as separator.
     *
     * @param uriList
     *            The base Uri.
     * @param relativeOrAbslouteUri
     *            The relative or absloute URI.
     * @return The appended Uri.
     * @throws URISyntaxException
     */
    public static StorageUri appendPathToUri(final StorageUri uriList, final String relativeOrAbsoluteUri,
            final String separator) throws URISyntaxException {
        return new StorageUri(appendPathToSingleUri(uriList.getPrimaryUri(), relativeOrAbsoluteUri, separator),
                appendPathToSingleUri(uriList.getSecondaryUri(), relativeOrAbsoluteUri, separator));
    }

    /**
     * Appends a path to a URI correctly using "/" as separator.
     *
     * @param uriList
     *            The base Uri.
     * @param relativeOrAbslouteUri
     *            The relative or absloute URI.
     * @return The appended Uri.
     * @throws URISyntaxException
     */
    public static URI appendPathToSingleUri(final URI uri, final String relativeOrAbsoluteUri)
            throws URISyntaxException {
        return appendPathToSingleUri(uri, relativeOrAbsoluteUri, "/");
    }

    /**
     * Appends a path to a URI correctly using the given separator.
     *
     * @param uri
     *            The base Uri.
     * @param relativeUri
     *            The relative URI.
     * @param separator
     *            the separator to use.
     * @return The appended Uri.
     * @throws URISyntaxException
     *             a valid Uri cannot be constructed
     */
    public static URI appendPathToSingleUri(final URI uri, final String relativeUri, final String separator)
            throws URISyntaxException {

        if (uri == null) {
            return null;
        }

        if (relativeUri == null || relativeUri.isEmpty()) {
            return uri;
        }

        if (uri.getPath().length() == 0 && relativeUri.startsWith(separator)) {
            return new URI(uri.getScheme(), uri.getAuthority(), relativeUri, uri.getRawQuery(), uri.getRawFragment());
        }

        final StringBuilder pathString = new StringBuilder(uri.getPath());
        if (uri.getPath().endsWith(separator)) {
            pathString.append(relativeUri);
        }
        else {
            pathString.append(separator);
            pathString.append(relativeUri);
        }

        return new URI(uri.getScheme(), uri.getAuthority(), pathString.toString(), uri.getQuery(), uri.getFragment());
    }

    /**
     * Gets the blob name from the URI.
     *
     * @param inURI
     *            the resource address
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return the blobs name
     * @throws URISyntaxException
     */
    public static String getBlobNameFromURI(final URI inURI, final boolean usePathStyleUris) throws URISyntaxException {
        return Utility.safeRelativize(new URI(getContainerURI(new StorageUri(inURI), usePathStyleUris).getPrimaryUri()
                .toString().concat("/")), inURI);
    }

    /**
     * Gets the canonical path for an object from the credentials.
     *
     * @param credentials
     *            the credentials to use.
     * @param absolutePath
     *            the Absolute path of the object.
     * @return the canonical path for an object from the credentials
     */
    public static String getCanonicalPathFromCredentials(final StorageCredentials credentials, final String absolutePath) {
        final String account = credentials.getAccountName();

        if (account == null) {
            final String errorMessage = SR.CANNOT_CREATE_SAS_FOR_GIVEN_CREDENTIALS;
            throw new IllegalArgumentException(errorMessage);
        }
        final StringBuilder builder = new StringBuilder("/");
        builder.append(account);
        builder.append(absolutePath);
        return builder.toString();
    }

    /**
     * Get the container name from address from the URI.
     *
     * @param resourceAddress
     *            The container Uri.
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return container name from address from the URI.
     * @throws IllegalArgumentException
     */
    public static String getContainerNameFromUri(final URI resourceAddress, final boolean usePathStyleUris) {
        return getResourceNameFromUri(resourceAddress, usePathStyleUris,
                String.format("Invalid blob address '%s', missing container information", resourceAddress));
    }

    /**
     * Gets the file name from the URI.
     *
     * @param resourceAddress
     *            the file URI
     * @param usePathStyleUris
     *            a value indicating if the address is a path style URI
     * @return the file's name
     */
    public static String getFileNameFromURI(final URI resourceAddress, final boolean usePathStyleUris) {
        // generate an array of the different levels of the path
        final String[] pathSegments = resourceAddress.getRawPath().split("/");

        // usePathStyleUris ? baseuri/accountname/sharename/objectname : accountname.baseuri/sharename/objectname
        final int shareIndex = usePathStyleUris ? 2 : 1;

        if (pathSegments.length - 1 <= shareIndex) {
            // legal file addresses cannot end with or before the sharename
            throw new IllegalArgumentException(String.format("Invalid file address '%s'.", resourceAddress));
        }
        else {
            // in a legal file address the lowest level is the filename
            return pathSegments[pathSegments.length - 1];
        }
    }

    /**
     * Get the name of the lowest level directory from the given directory address.
     *
     * @param resourceAddress
     *            the directory URI
     * @param usePathStyleUris
     *            a value indicating if the address is a path style URI
     * @return directory name from address from the URI
     */
    public static String getDirectoryNameFromURI(final URI resourceAddress, final boolean usePathStyleUris) {
        // generate an array of the different levels of the path
        final String[] pathSegments = resourceAddress.getRawPath().split("/");

        // usePathStyleUris ? baseuri/accountname/sharename/objectname : accountname.baseuri/sharename/objectname
        final int shareIndex = usePathStyleUris ? 2 : 1;

        if (pathSegments.length - 1 < shareIndex) {
            // if the sharename is missing or too close to the end
            throw new IllegalArgumentException(String.format("Invalid directory address '%s'.", resourceAddress));
        }
        else if (pathSegments.length - 1 == shareIndex) {
            // this is the root directory; it has no name
            return "";
        }
        else {
            // in a legal directory address the lowest level is the directory
            return pathSegments[pathSegments.length - 1];
        }
    }

    /**
     * Get the share name from address from the URI.
     *
     * @param resourceAddress
     *            The share Uri.
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return share name from address from the URI.
     * @throws IllegalArgumentException
     */
    public static String getShareNameFromUri(final URI resourceAddress, final boolean usePathStyleUris) {
        return getResourceNameFromUri(resourceAddress, usePathStyleUris,
                String.format("Invalid file address '%s', missing share information", resourceAddress));
    }

    /**
     * Get the table name from address from the URI.
     *
     * @param resourceAddress
     *            The table Uri.
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return table name from address from the URI.
     * @throws IllegalArgumentException
     */
    public static String getTableNameFromUri(final URI resourceAddress, final boolean usePathStyleUris) {
        return getResourceNameFromUri(resourceAddress, usePathStyleUris,
                String.format("Invalid table address '%s', missing table information", resourceAddress));
    }

    /**
     * Get the container, queue or table name from address from the URI.
     *
     * @param resourceAddress
     *            The queue Uri.
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return container name from address from the URI.
     * @throws IllegalArgumentException
     */
    private static String getResourceNameFromUri(final URI resourceAddress, final boolean usePathStyleUris,
            final String error) {
        Utility.assertNotNull("resourceAddress", resourceAddress);

        final String[] pathSegments = resourceAddress.getRawPath().split("/");

        final int expectedPartsLength = usePathStyleUris ? 3 : 2;

        if (pathSegments.length < expectedPartsLength) {
            throw new IllegalArgumentException(error);
        }

        final String resourceName = usePathStyleUris ? pathSegments[2] : pathSegments[1];

        return Utility.trimEnd(resourceName, '/');
    }

    /**
     * Gets the container URI from a blob address
     *
     * @param blobAddress
     *            the blob address
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return the container URI from a blob address
     * @throws URISyntaxException
     */
    public static StorageUri getContainerURI(final StorageUri blobAddress, final boolean usePathStyleUris)
            throws URISyntaxException {
        final String containerName = getContainerNameFromUri(blobAddress.getPrimaryUri(), usePathStyleUris);

        final StorageUri containerUri = appendPathToUri(getServiceClientBaseAddress(blobAddress, usePathStyleUris),
                containerName);
        return containerUri;
    }

    /**
     * Gets the share URI from a file address
     *
     * @param fileAddress
     *            the file address
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return the share URI from a file address
     * @throws URISyntaxException
     */
    public static StorageUri getShareURI(final StorageUri fileAddress, final boolean usePathStyleUris)
            throws URISyntaxException {
        final String shareName = getShareNameFromUri(fileAddress.getPrimaryUri(), usePathStyleUris);

        final StorageUri shareUri = appendPathToUri(getServiceClientBaseAddress(fileAddress, usePathStyleUris),
                shareName);
        return shareUri;
    }

    /**
     * Get the service client address from a complete Uri.
     *
     * @param address
     *            Complete address of the resource.
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return the service client address from a complete Uri.
     * @throws URISyntaxException
     */
    public static String getServiceClientBaseAddress(final URI address, final boolean usePathStyleUris)
            throws URISyntaxException {
        if (address == null) {
            return null;
        }

        if (usePathStyleUris) {
            final String[] pathSegments = address.getRawPath().split("/");

            if (pathSegments.length < 2) {
                final String error = String.format(SR.PATH_STYLE_URI_MISSING_ACCOUNT_INFORMATION);
                throw new IllegalArgumentException(error);
            }

            final StringBuilder completeAddress = new StringBuilder(new URI(address.getScheme(),
                    address.getAuthority(), null, null, null).toString());
            completeAddress.append("/");
            completeAddress.append(Utility.trimEnd(pathSegments[1], '/'));

            return completeAddress.toString();
        }
        else {
            return new URI(address.getScheme(), address.getAuthority(), null, null, null).toString();
        }
    }

    /**
     * Get the service client address from a complete Uri.
     *
     * @param address
     *            Complete address of the resource.
     * @param usePathStyleUris
     *            a value indicating if the address is a path style uri.
     * @return the service client address from a complete Uri.
     * @throws URISyntaxException
     */
    public static StorageUri getServiceClientBaseAddress(final StorageUri addressUri, final boolean usePathStyleUris)
            throws URISyntaxException {
        return new StorageUri(new URI(getServiceClientBaseAddress(addressUri.getPrimaryUri(), usePathStyleUris)),
                addressUri.getSecondaryUri() != null ?
                new URI(getServiceClientBaseAddress(addressUri.getSecondaryUri(), usePathStyleUris)) : null);
    }

    /**
     * Parses a query string into a one to many hashmap.
     *
     * @param parseString
     *            the string to parse
     * @return a HashMap<String, String[]> of the key values.
     * @throws StorageException
     */
    public static HashMap<String, String[]> parseQueryString(String parseString) throws StorageException {
        final HashMap<String, String[]> retVals = new HashMap<String, String[]>();
        if (Utility.isNullOrEmpty(parseString)) {
            return retVals;
        }

        // 1. Remove ? if present
        final int queryDex = parseString.indexOf("?");
        if (queryDex >= 0 && parseString.length() > 0) {
            parseString = parseString.substring(queryDex + 1);
        }

        // 2. split name value pairs by splitting on the 'c&' character
        final String[] valuePairs = parseString.contains("&") ? parseString.split("&") : parseString.split(";");

        // 3. for each field value pair parse into appropriate map entries
        for (int m = 0; m < valuePairs.length; m++) {
            final int equalDex = valuePairs[m].indexOf("=");

            if (equalDex < 0 || equalDex == valuePairs[m].length() - 1) {
                continue;
            }

            String key = valuePairs[m].substring(0, equalDex);
            String value = valuePairs[m].substring(equalDex + 1);

            key = Utility.safeDecode(key);
            value = Utility.safeDecode(value);

            // 3.1 add to map
            String[] values = retVals.get(key);

            if (values == null) {
                values = new String[] { value };
                if (!value.equals(Constants.EMPTY_STRING)) {
                    retVals.put(key, values);
                }
            }
            else if (!value.equals(Constants.EMPTY_STRING)) {
                final String[] newValues = new String[values.length + 1];
                for (int j = 0; j < values.length; j++) {
                    newValues[j] = values[j];
                }

                newValues[newValues.length] = value;
            }
        }

        return retVals;
    }

    /**
     * Strips the Query and Fragment from the uri.
     *
     * @param inUri
     *            the uri to alter
     * @return the stripped uri.
     * @throws StorageException
     */
    public static URI stripSingleURIQueryAndFragment(final URI inUri) throws StorageException {
        if (inUri == null) {
            return null;
        }
        try {
            return new URI(inUri.getScheme(), inUri.getAuthority(), inUri.getPath(), null, null);
        }
        catch (final URISyntaxException e) {
            throw Utility.generateNewUnexpectedStorageException(e);
        }
    }

    /**
     * Strips the Query and Fragment from the uri.
     *
     * @param inUri
     *            the uri to alter
     * @return the stripped uri.
     * @throws StorageException
     */
    public static StorageUri stripURIQueryAndFragment(final StorageUri inUri) throws StorageException {
        return new StorageUri(stripSingleURIQueryAndFragment(inUri.getPrimaryUri()),
                stripSingleURIQueryAndFragment(inUri.getSecondaryUri()));
    }

    /**
     * Private Default Ctor.
     */
    private PathUtility() {
        // No op
    }
}


/**
 * Copyright Microsoft Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.microsoft.azure.storage.core;

import java.security.InvalidKeyException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

import com.microsoft.azure.storage.CloudStorageAccount;
import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.IPRange;
import com.microsoft.azure.storage.ServiceClient;
import com.microsoft.azure.storage.SharedAccessAccountPolicy;
import com.microsoft.azure.storage.SharedAccessHeaders;
import com.microsoft.azure.storage.SharedAccessPolicy;
import com.microsoft.azure.storage.SharedAccessProtocols;
import com.microsoft.azure.storage.StorageCredentials;
import com.microsoft.azure.storage.StorageCredentialsSharedAccessSignature;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.StorageUri;
import com.microsoft.azure.storage.queue.SharedAccessQueuePolicy;
import com.microsoft.azure.storage.table.SharedAccessTablePolicy;

/**
 * RESERVED FOR INTERNAL USE. Contains helper methods for implementing shared access signatures.
 */
public class SharedAccessSignatureHelper {
    /**
     * Get the signature hash embedded inside the Shared Access Signature for a {@link CloudStorageAccount}.
     *
     * @param policy
     *            The shared access policy to hash.
     * @param ipRange
     *            An optional range of IP addresses.
     * @param protocols
     *            An optional restriction of allowed protocols.
     * @param signature
     *            The signature to use.
     *
     * @return The finished query builder
     * @throws InvalidKeyException
     * @throws StorageException
     */
    public static UriQueryBuilder generateSharedAccessSignatureForAccount(
            final SharedAccessAccountPolicy policy, final String signature)
            throws StorageException {

        Utility.assertNotNull("policy", policy);

        Utility.assertNotNull("signature", signature);

        String permissions = null;
        Date startTime = null;
        Date expiryTime = null;
        IPRange ipRange = null;
        SharedAccessProtocols protocols = null;
        String services = null;
        String resourceTypes = null;
        if (policy != null) {
            permissions = policy.permissionsToString();
            startTime = policy.getSharedAccessStartTime();
            expiryTime = policy.getSharedAccessExpiryTime();
            ipRange = policy.getRange();
            protocols = policy.getProtocols();
            services = policy.servicesToString();
            resourceTypes = policy.resourceTypesToString();
        }

        final UriQueryBuilder builder = new UriQueryBuilder();

        builder.add(Constants.QueryConstants.SIGNED_VERSION, Constants.HeaderConstants.TARGET_STORAGE_VERSION);

        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_SERVICE, services);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_RESOURCE_TYPE, resourceTypes);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_PERMISSIONS, permissions);

        final String startString = Utility.getUTCTimeOrEmpty(startTime);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_START, startString);

        final String stopString = Utility.getUTCTimeOrEmpty(expiryTime);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_EXPIRY, stopString);

        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_RESOURCE, resourceTypes);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_IP, ipRange != null ? ipRange.toString() : null);
        addIfNotNullOrEmpty(
                builder, Constants.QueryConstants.SIGNED_PROTOCOLS, protocols != null ? protocols.toString() : null);

        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNATURE, signature);

        return builder;
    }

    /**
     * Get the complete query builder for creating the Shared Access Signature query.
     *
     * @param policy
     *            The shared access policy for the shared access signature.
     * @param headers
     *            The optional header values to set for a blob or file accessed with this shared access signature.
     * @param groupPolicyIdentifier
     *            An optional identifier for the policy.
     * @param resourceType
     *            Either "b" for blobs, "c" for containers, "f" for files, or "s" for shares.
     * @param ipRange
     *            The range of IP addresses for the shared access signature.
     * @param protocols
     *            The Internet protocols for the shared access signature.
     * @param signature
     *            The signature to use.
     *
     * @return The finished query builder
     * @throws IllegalArgumentException
     * @throws StorageException
     */
    public static UriQueryBuilder generateSharedAccessSignatureForBlobAndFile(
            final SharedAccessPolicy policy, final SharedAccessHeaders headers, final String groupPolicyIdentifier,
            final String resourceType, final IPRange ipRange, final SharedAccessProtocols protocols, final String signature)
            throws StorageException {

        Utility.assertNotNullOrEmpty("resourceType", resourceType);

        return generateSharedAccessSignatureHelper(policy, null /* startPartitionKey */, null /* startRowKey */,
                null /* endPartitionKey */, null /* endRowKey */, groupPolicyIdentifier, resourceType, ipRange,
                protocols, null /* tableName */, signature, headers);
    }

    /**
     * Get the complete query builder for creating the Shared Access Signature query.
     *
     * @param policy
     *            The shared access policy for the shared access signature.
     * @param groupPolicyIdentifier
     *            An optional identifier for the policy.
     * @param ipRange
     *            The range of IP addresses for the shared access signature.
     * @param protocols
     *            The Internet protocols for the shared access signature.
     * @param signature
     *            The signature to use.
     *
     * @return The finished query builder
     * @throws IllegalArgumentException
     * @throws StorageException
     */
    public static UriQueryBuilder generateSharedAccessSignatureForQueue(
            final SharedAccessQueuePolicy policy, final String groupPolicyIdentifier, final IPRange ipRange,
            final SharedAccessProtocols protocols, final String signature)
            throws StorageException {

        return generateSharedAccessSignatureHelper(policy, null /* startPartitionKey */, null /* startRowKey */,
                null /* endPartitionKey */, null /* endRowKey */, groupPolicyIdentifier, null /* resourceType */,
                ipRange, protocols, null /* tableName */, signature, null /* headers */);
    }

    /**
     * Get the complete query builder for creating the Shared Access Signature query.
     *
     * @param policy
     *            The shared access policy for the shared access signature.
     * @param startPartitionKey
     *            An optional restriction of the beginning of the range of partition keys to include.
     * @param startRowKey
     *            An optional restriction of the beginning of the range of row keys to include.
     * @param endPartitionKey
     *            An optional restriction of the end of the range of partition keys to include.
     * @param endRowKey
     *            An optional restriction of the end of the range of row keys to include.
     * @param accessPolicyIdentifier
     *            An optional identifier for the policy.
     * @param ipRange
     *            The range of IP addresses for the shared access signature.
     * @param protocols
     *            The Internet protocols for the shared access signature.
     * @param tableName
     *            The table name.
     * @param signature
     *            The signature to use.
     *
     * @return The finished query builder
     * @throws IllegalArgumentException
     * @throws StorageException
     */
    public static UriQueryBuilder generateSharedAccessSignatureForTable(
            final SharedAccessTablePolicy policy, final String startPartitionKey, final String startRowKey,
            final String endPartitionKey, final String endRowKey, final String accessPolicyIdentifier,
            final IPRange ipRange, final SharedAccessProtocols protocols, final String tableName, final String signature)
            throws StorageException {

        Utility.assertNotNull("tableName", tableName);
        return generateSharedAccessSignatureHelper(
                policy, startPartitionKey, startRowKey, endPartitionKey, endRowKey, accessPolicyIdentifier,
                null /* resourceType */, ipRange, protocols, tableName, signature, null /* headers */);
    }

    /**
     * Get the signature hash embedded inside the Shared Access Signature for a {@link CloudStorageAccount}.
     *
     * @param accountName
     *            The name of the account to use for the SAS.
     * @param policy
     *            The shared access policy to hash.
     * @param ipRange
     *            An optional range of IP addresses.
     * @param protocols
     *            An optional restriction of allowed protocols.
     * @param creds
     *            The {@link StorageCredentials} associated with the object.
     *
     * @return The signature hash embedded inside the Shared Access Signature.
     * @throws InvalidKeyException
     * @throws StorageException
     */
    public static String generateSharedAccessSignatureHashForAccount(
            final String accountName, final SharedAccessAccountPolicy policy, final StorageCredentials creds)
            throws InvalidKeyException, StorageException {
        Utility.assertNotNullOrEmpty("resource", accountName);
        Utility.assertNotNull("credentials", creds);

        String permissions = null;
        Date startTime = null;
        Date expiryTime = null;
        IPRange ipRange = null;
        SharedAccessProtocols protocols = null;
        String services = null;
        String resourceTypes = null;

        if (policy != null) {
            permissions = policy.permissionsToString();
            startTime = policy.getSharedAccessStartTime();
            expiryTime = policy.getSharedAccessExpiryTime();
            ipRange = policy.getRange();
            protocols = policy.getProtocols();
            services = policy.servicesToString();
            resourceTypes = policy.resourceTypesToString();
        }

        String stringToSign = String.format("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n",
                accountName, permissions == null ? Constants.EMPTY_STRING : permissions, services, resourceTypes,
                Utility.getUTCTimeOrEmpty(startTime), Utility.getUTCTimeOrEmpty(expiryTime),
                ipRange == null ? Constants.EMPTY_STRING : ipRange.toString(),
                protocols == null ? Constants.EMPTY_STRING : protocols.toString(),
                Constants.HeaderConstants.TARGET_STORAGE_VERSION);

        return generateSharedAccessSignatureHashHelper(stringToSign, creds);
    }

    /**
     * Get the signature hash embedded inside the Shared Access Signature for the blob or file service.
     *
     * @param policy
     *            The shared access policy to hash.
     * @param headers
     *            The optional header values to set for a blob or file accessed with this shared access signature.
     * @param accessPolicyIdentifier
     *            An optional identifier for the policy.
     * @param resourceName
     *            The resource name.
     * @param ipRange
     *            The range of IP addresses to hash.
     * @param protocols
     *            The Internet protocols to hash.
     * @param client
     *            The ServiceClient associated with the object.
     *
     * @return The signature hash embedded inside the Shared Access Signature.
     * @throws InvalidKeyException
     * @throws StorageException
     */
    public static String generateSharedAccessSignatureHashForBlobAndFile(final SharedAccessPolicy policy,
            SharedAccessHeaders headers, final String accessPolicyIdentifier, final String resourceName,
            final IPRange ipRange, final SharedAccessProtocols protocols, final ServiceClient client)
            throws InvalidKeyException, StorageException {

        String stringToSign = generateSharedAccessSignatureStringToSign(
                policy, resourceName, ipRange, protocols, accessPolicyIdentifier);

        String cacheControl = null;
        String contentDisposition = null;
        String contentEncoding = null;
        String contentLanguage = null;
        String contentType = null;
        if (headers != null) {
            cacheControl = headers.getCacheControl();
            contentDisposition = headers.getContentDisposition();
            contentEncoding = headers.getContentEncoding();
            contentLanguage = headers.getContentLanguage();
            contentType = headers.getContentType();
        }

        stringToSign = String.format("%s\n%s\n%s\n%s\n%s\n%s", stringToSign,
                cacheControl == null ? Constants.EMPTY_STRING : cacheControl,
                contentDisposition == null ? Constants.EMPTY_STRING : contentDisposition,
                contentEncoding == null ? Constants.EMPTY_STRING : contentEncoding,
                contentLanguage == null ? Constants.EMPTY_STRING : contentLanguage,
                contentType == null ? Constants.EMPTY_STRING : contentType);

        return generateSharedAccessSignatureHashHelper(stringToSign, client.getCredentials());
    }

    /**
     * Get the signature hash embedded inside the Shared Access Signature for queue service.
     *
     * @param policy
     *            The shared access policy to hash.
     * @param accessPolicyIdentifier
     *            An optional identifier for the policy.
     * @param resourceName
     *            The resource name.
     * @param ipRange
     *            The range of IP addresses to hash.
     * @param protocols
     *            The Internet protocols to hash.
     * @param client
     *            The ServiceClient associated with the object.
     *
     * @return The signature hash embedded inside the Shared Access Signature.
     * @throws InvalidKeyException
     * @throws StorageException
     */

    public static String generateSharedAccessSignatureHashForQueue(
            final SharedAccessQueuePolicy policy, final String accessPolicyIdentifier, final String resourceName,
            final IPRange ipRange, final SharedAccessProtocols protocols, final ServiceClient client)
            throws InvalidKeyException, StorageException {

        final String stringToSign = generateSharedAccessSignatureStringToSign(
                policy, resourceName, ipRange, protocols, accessPolicyIdentifier);

        return generateSharedAccessSignatureHashHelper(stringToSign, client.getCredentials());
    }

    /**
     * Get the signature hash embedded inside the Shared Access Signature for the table service.
     *
     * @param policy
     *            The shared access policy to hash.
     * @param accessPolicyIdentifier
     *            An optional identifier for the policy.
     * @param resourceName
     *            The resource name.
     * @param ipRange
     *            The range of IP addresses to hash.
     * @param protocols
     *            The Internet protocols to hash.
     * @param startPartitionKey
     *            An optional restriction of the beginning of the range of partition keys to hash.
     * @param startRowKey
     *            An optional restriction of the beginning of the range of row keys to hash.
     * @param endPartitionKey
     *            An optional restriction of the end of the range of partition keys to hash.
     * @param endRowKey
     *            An optional restriction of the end of the range of row keys to hash.
     * @param client
     *            The ServiceClient associated with the object.
     *
     * @return The signature hash embedded inside the Shared Access Signature.
     * @throws InvalidKeyException
     * @throws StorageException
     */
    public static String generateSharedAccessSignatureHashForTable(
            final SharedAccessTablePolicy policy, final String accessPolicyIdentifier, final String resourceName,
            final IPRange ipRange, final SharedAccessProtocols protocols, final String startPartitionKey,
            final String startRowKey, final String endPartitionKey, final String endRowKey, final ServiceClient client)
            throws InvalidKeyException, StorageException {

        String stringToSign = generateSharedAccessSignatureStringToSign(
                policy, resourceName, ipRange, protocols, accessPolicyIdentifier);

        stringToSign = String.format("%s\n%s\n%s\n%s\n%s", stringToSign,
                startPartitionKey == null ? Constants.EMPTY_STRING : startPartitionKey,
                startRowKey == null ? Constants.EMPTY_STRING : startRowKey,
                endPartitionKey == null ? Constants.EMPTY_STRING : endPartitionKey,
                endRowKey == null ? Constants.EMPTY_STRING : endRowKey);

        return generateSharedAccessSignatureHashHelper(stringToSign, client.getCredentials());
    }

    /**
     * Parses the query parameters and populates a StorageCredentialsSharedAccessSignature object if one is present.
     *
     * @param completeUri
     *            A {@link StorageUri} object which represents the complete Uri.
     *
     * @return The StorageCredentialsSharedAccessSignature if one is present, otherwise null.
     * @throws StorageException
     *             An exception representing any error which occurred during the operation.
     */
    public static StorageCredentialsSharedAccessSignature parseQuery(final StorageUri completeUri) throws StorageException {
        final HashMap<String, String[]> queryParameters = PathUtility.parseQueryString(completeUri.getQuery());
        return parseQuery(queryParameters);
    }

    /**
     * Parses the query parameters and populates a StorageCredentialsSharedAccessSignature object if one is present.
     *
     * @param queryParams
     *            The parameters to parse.
     *
     * @return The StorageCredentialsSharedAccessSignature if one is present, otherwise null.
     * @throws StorageException
     *             An exception representing any error which occurred during the operation.
     */
    public static StorageCredentialsSharedAccessSignature parseQuery(final HashMap<String, String[]> queryParams)
            throws StorageException {

        boolean sasParameterFound = false;
        List<String> removeList = new ArrayList<String>();
        for (final Entry<String, String[]> entry : queryParams.entrySet()) {
            final String lowerKey = entry.getKey().toLowerCase(Utility.LOCALE_US);

            if (lowerKey.equals(Constants.QueryConstants.SIGNATURE)) {
                sasParameterFound = true;
            } else if (lowerKey.equals(Constants.QueryConstants.COMPONENT)) {
                removeList.add(entry.getKey());
            } else if (lowerKey.equals(Constants.QueryConstants.RESOURCETYPE)) {
                removeList.add(entry.getKey());
            } else if (lowerKey.equals(Constants.QueryConstants.SNAPSHOT)) {
                removeList.add(entry.getKey());
            } else if (lowerKey.equals(Constants.QueryConstants.API_VERSION)) {
                removeList.add(entry.getKey());
            }
        }

        for (String removeParam : removeList) {
            queryParams.remove(removeParam);
        }

        if (sasParameterFound) {
            final UriQueryBuilder builder = new UriQueryBuilder();

            StringBuilder values = new StringBuilder();
            for (final Entry<String, String[]> entry : queryParams.entrySet()) {
                values.setLength(0);
                for (int i = 0; i < entry.getValue().length; i++) {
                    values.append(entry.getValue()[i]);
                    values.append(',');
                }
                values.deleteCharAt(values.length() - 1);

                addIfNotNullOrEmpty(builder, entry.getKey().toLowerCase(), values.toString());
            }

            return new StorageCredentialsSharedAccessSignature(builder.toString());
        }

        return null;
    }

    /**
     * Helper to add a name/value pair to a <code>UriQueryBuilder</code> if the value is not null or empty.
     *
     * @param builder
     *            The builder to add to.
     * @param name
     *            The name to add.
     * @param val
     *            The value to add if not null or empty.
     *
     * @throws StorageException
     *            An exception representing any error which occurred during the operation.
     */
    private static void addIfNotNullOrEmpty(UriQueryBuilder builder, String name, String val) throws StorageException {
        if (!Utility.isNullOrEmpty(val)) {
            builder.add(name, val);
        }
    }

    /**
     * Get the complete query builder for creating the Shared Access Signature query.
     *
     * @param policy
     *            A {@link SharedAccessPolicy} containing the permissions for the SAS.
     * @param startPartitionKey
     *            The start partition key for a shared access signature URI.
     * @param startRowKey
     *            The start row key for a shared access signature URI.
     * @param endPartitionKey
     *            The end partition key for a shared access signature URI.
     * @param endRowKey
     *            The end row key for a shared access signature URI.
     * @param accessPolicyIdentifier
     *            An optional identifier for the policy.
     * @param resourceType
     *            The resource type for a shared access signature URI.
     * @param ipRange
     *            The range of IP addresses for the shared access signature.
     * @param protocols
     *            The Internet protocols for the shared access signature.
     * @param tableName
     *            The table name.
     * @param signature
     *            The signature hash.
     * @param headers
     *            Optional blob or file headers.
     *
     * @return The finished query builder.
     * @throws StorageException
     *            An exception representing any error which occurred during the operation.
     */
    private static UriQueryBuilder generateSharedAccessSignatureHelper(
            final SharedAccessPolicy policy, final String startPartitionKey, final String startRowKey,
            final String endPartitionKey, final String endRowKey, final String accessPolicyIdentifier,
            final String resourceType, final IPRange ipRange, final SharedAccessProtocols protocols,
            final String tableName, final String signature, final SharedAccessHeaders headers)
            throws StorageException {

        Utility.assertNotNull("signature", signature);

        String permissions = null;
        Date startTime = null;
        Date expiryTime = null;
        if (policy != null) {
            permissions = policy.permissionsToString();
            startTime = policy.getSharedAccessStartTime();
            expiryTime = policy.getSharedAccessExpiryTime();
        }

        final UriQueryBuilder builder = new UriQueryBuilder();

        builder.add(Constants.QueryConstants.SIGNED_VERSION, Constants.HeaderConstants.TARGET_STORAGE_VERSION);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_PERMISSIONS, permissions);

        final String startString = Utility.getUTCTimeOrEmpty(startTime);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_START, startString);

        final String stopString = Utility.getUTCTimeOrEmpty(expiryTime);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_EXPIRY, stopString);

        addIfNotNullOrEmpty(builder, Constants.QueryConstants.START_PARTITION_KEY, startPartitionKey);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.START_ROW_KEY, startRowKey);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.END_PARTITION_KEY, endPartitionKey);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.END_ROW_KEY, endRowKey);

        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_IDENTIFIER, accessPolicyIdentifier);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_RESOURCE, resourceType);
        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNED_IP, ipRange != null ? ipRange.toString() : null);
        addIfNotNullOrEmpty(
                builder, Constants.QueryConstants.SIGNED_PROTOCOLS, protocols != null ? protocols.toString() : null);

        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SAS_TABLE_NAME, tableName);

        if (headers != null) {
            addIfNotNullOrEmpty(builder, Constants.QueryConstants.CACHE_CONTROL, headers.getCacheControl());
            addIfNotNullOrEmpty(builder, Constants.QueryConstants.CONTENT_TYPE, headers.getContentType());
            addIfNotNullOrEmpty(builder, Constants.QueryConstants.CONTENT_ENCODING, headers.getContentEncoding());
            addIfNotNullOrEmpty(builder, Constants.QueryConstants.CONTENT_LANGUAGE, headers.getContentLanguage());
            addIfNotNullOrEmpty(builder, Constants.QueryConstants.CONTENT_DISPOSITION, headers.getContentDisposition());
        }

        addIfNotNullOrEmpty(builder, Constants.QueryConstants.SIGNATURE, signature);

        return builder;
    }

    /**
     * Get the signature hash embedded inside the Shared Access Signature.
     *
     * @param stringToSign
     *            The string to decode and hash
     * @param creds
     *            Reference to the {@link StorageCredentials.}.
     *
     * @return The signature hash embedded inside the Shared Access Signature.
     *
     * @throws InvalidKeyException
     * @throws StorageException
     */
    private static String generateSharedAccessSignatureHashHelper(String stringToSign, final StorageCredentials creds)
            throws StorageException, InvalidKeyException {

        Utility.assertNotNull("credentials", creds);

        Logger.debug(null, LogConstants.SIGNING, stringToSign);

        stringToSign = Utility.safeDecode(stringToSign);
        final String signature = StorageCredentialsHelper.computeHmac256(creds, stringToSign);

        Logger.verbose(null, LogConstants.SIGNING, stringToSign);

        return signature;
    }

    /**
     * Get the signature hash embedded inside the Shared Access Signature.
     *
     * @param policy
     *            A {@link SharedAccessPolicy} containing the permissions for the SAS.
     * @param resource
     *            The canonical resource (or resource type) string, unescaped.
     * @param ipRange
     *            The range of IP addresses to hash.
     * @param protocols
     *            The Internet protocols to hash.
     * @param headers
     *            The optional header values to set for a blob or file accessed with this shared access signature.
     * @param accessPolicyIdentifier
     *            An optional identifier for the policy.
     *
     * @return The signature hash embedded inside the Shared Access Signature.
     *
     * @throws InvalidKeyException
     * @throws StorageException
     */
    private static String generateSharedAccessSignatureStringToSign(
            final SharedAccessPolicy policy, final String resource, final IPRange ipRange,
            final SharedAccessProtocols protocols, final String accessPolicyIdentifier)
            throws InvalidKeyException, StorageException {

        Utility.assertNotNullOrEmpty("resource", resource);

        String permissions = null;
        Date startTime = null;
        Date expiryTime = null;

        if (policy != null) {
            permissions = policy.permissionsToString();
            startTime = policy.getSharedAccessStartTime();
            expiryTime = policy.getSharedAccessExpiryTime();
        }

        String stringToSign = String.format("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s",
                permissions == null ? Constants.EMPTY_STRING : permissions,
                Utility.getUTCTimeOrEmpty(startTime), Utility.getUTCTimeOrEmpty(expiryTime), resource,
                accessPolicyIdentifier == null ? Constants.EMPTY_STRING : accessPolicyIdentifier,
                ipRange == null ? Constants.EMPTY_STRING : ipRange.toString(),
                protocols == null ? Constants.EMPTY_STRING : protocols.toString(),
                Constants.HeaderConstants.TARGET_STORAGE_VERSION);

        return stringToSign;
    }

    /**
     * Private Default Ctor.
     */
    private SharedAccessSignatureHelper() {
        // No op
    }
}