/**
 * 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.table;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map.Entry;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.StorageErrorCodeStrings;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.core.JsonUtilities;
import com.microsoft.azure.storage.core.SR;

/**
 * Reserved for internal use. A class used to read Table entities.
 */
final class TableDeserializer {

    /**
     * Used to create Json parsers and generators.
     */
    private static JsonFactory jsonFactory = new JsonFactory();

    /**
     * Reserved for internal use. Parses the operation response as a collection of entities. Reads entity data from the
     * specified input stream using the specified class type and optionally projects each entity result with the
     * specified resolver into an {@link ODataPayload} containing a collection of {@link TableResult} objects.
     *
     * @param inStream
     *            The InputStream to read the data to parse from.
     * @param clazzType
     *            The class type T implementing {@link TableEntity} for the entities returned. Set to
     *            null to ignore the returned entities and copy only response properties into the
     *            {@link TableResult} objects.
     * @param resolver
     *            An {@link EntityResolver} instance to project the entities into instances of type R. Set
     *            to null to return the entities as instances of the class type T.
     * @param options
     *            A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout
     *            settings for the operation.
     * @param opContext
     *            An {@link OperationContext} object used to track the execution of the operation.
     * @return
     *         An {@link ODataPayload} containing a collection of {@link TableResult} objects with the parsed operation
     *         response.
     * @throws InstantiationException
     *             if an error occurs while constructing the result.
     * @throws IllegalAccessException
     *             if an error occurs in reflection while parsing the result.
     * @throws StorageException
     *             if a storage service error occurs.
     * @throws IOException
     *             if an error occurs while accessing the stream.
     * @throws JsonParseException
     *             if an error occurs while parsing the stream.
     */
    @SuppressWarnings("unchecked")
    static <T extends TableEntity, R> ODataPayload<?> parseQueryResponse(final InputStream inStream,
            final TableRequestOptions options, Class<T> clazzType, final EntityResolver<R> resolver,
            final OperationContext opContext) throws InstantiationException, IllegalAccessException,
            StorageException, JsonParseException, IOException {
        ODataPayload<T> corePayload = null;
        ODataPayload<R> resolvedPayload = null;
        ODataPayload<?> commonPayload = null;

        JsonParser parser = createJsonParserFromStream(inStream);

        try {

            if (resolver != null) {
                resolvedPayload = new ODataPayload<R>();
                commonPayload = resolvedPayload;
            }
            else {
                corePayload = new ODataPayload<T>();
                commonPayload = corePayload;
            }

            if (!parser.hasCurrentToken()) {
                parser.nextToken();
            }

            JsonUtilities.assertIsStartObjectJsonToken(parser);

            // move into data
            parser.nextToken();

            // if there is a clazz type and if JsonNoMetadata, create a classProperties dictionary to use for type inference once
            // instead of querying the cache many times
            HashMap<String, PropertyPair> classProperties = null;
            if (options.getTablePayloadFormat() == TablePayloadFormat.JsonNoMetadata && clazzType != null) {
                classProperties = PropertyPair.generatePropertyPairs(clazzType);
            }

            while (parser.getCurrentToken() != null) {
                if (parser.getCurrentToken() == JsonToken.FIELD_NAME
                        && parser.getCurrentName().equals(ODataConstants.VALUE)) {
                    // move to start of array
                    parser.nextToken();

                    JsonUtilities.assertIsStartArrayJsonToken(parser);

                    // go to properties
                    parser.nextToken();

                    while (parser.getCurrentToken() == JsonToken.START_OBJECT) {
                        final TableResult res = parseJsonEntity(parser, clazzType, classProperties, resolver, options,
                                opContext);
                        if (corePayload != null) {
                            corePayload.tableResults.add(res);
                        }

                        if (resolver != null) {
                            resolvedPayload.results.add((R) res.getResult());
                        }
                        else {
                            corePayload.results.add((T) res.getResult());
                        }

                        parser.nextToken();
                    }

                    JsonUtilities.assertIsEndArrayJsonToken(parser);
                }

                parser.nextToken();
            }
        }
        finally {
            parser.close();
        }

        return commonPayload;
    }

    /**
     * Reserved for internal use. Parses the operation response as an entity. Reads entity data from the specified
     * JsonParser using the specified class type and optionally projects the entity result with the
     * specified resolver into a {@link TableResult} object.
     *
     * @param inStream
     *            The InputStream to read the data to parse from.
     * @param format
     *            The {@link TablePayloadFormat} to use for parsing.
     * @param options
     *            A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout
     *            settings for the operation.
     * @param httpStatusCode
     *            The HTTP status code returned with the operation response.
     * @param clazzType
     *            The class type T implementing {@link TableEntity} for the entity returned. Set to
     *            null to ignore the returned entity and copy only response properties into the
     *            {@link TableResult} object.
     * @param resolver
     *            An {@link EntityResolver} instance to project the entity into an instance of type R. Set
     *            to null to return the entitys as instance of the class type T.
     * @param opContext
     *            An {@link OperationContext} object used to track the execution of the operation.
     * @return
     *         A {@link TableResult} object with the parsed operation response.
     * @throws InstantiationException
     *             if an error occurs while constructing the result.
     * @throws IllegalAccessException
     *             if an error occurs in reflection while parsing the result.
     * @throws StorageException
     *             if a storage service error occurs.
     * @throws IOException
     *             if an error occurs while accessing the stream.
     * @throws JsonParseException
     *             if an error occurs while parsing the stream.
     */
    static <T extends TableEntity, R> TableResult parseSingleOpResponse(final InputStream inStream,
            final TableRequestOptions options, final int httpStatusCode, final Class<T> clazzType,
            final EntityResolver<R> resolver, final OperationContext opContext) throws
            InstantiationException, IllegalAccessException, StorageException, JsonParseException, IOException {
        JsonParser parser = createJsonParserFromStream(inStream);

        try {
            final TableResult res = parseJsonEntity(parser, clazzType,
                    null /*HashMap<String, PropertyPair> classProperties*/, resolver, options, opContext);
            res.setHttpStatusCode(httpStatusCode);
            return res;
        }
        finally {
            parser.close();
        }
    }

    /**
     * Reserved for internal use. Parses the operation response as an entity. Parses the result returned in the
     * specified stream in JSON format into a {@link TableResult} containing an entity of the specified class type
     * projected using the specified resolver.
     *
     * @param parser
     *            The JsonParser to read the data to parse from.
     * @param clazzType
     *            The class type T implementing {@link TableEntity} for the entity returned. Set to
     *            null to ignore the returned entity and copy only response properties into the
     *            {@link TableResult} object.
     * @param resolver
     *            An {@link EntityResolver} instance to project the entity into an instance of type R. Set
     *            to null to return the entity as an instance of the class type T.
     * @param options
     *            A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout
     *            settings for the operation.
     * @param opContext
     *            An {@link OperationContext} object used to track the execution of the operation.
     * @return
     *         A {@link TableResult} containing the parsed entity result of the operation.
     * @throws IOException
     *             if an error occurs while accessing the stream.
     * @throws InstantiationException
     *             if an error occurs while constructing the result.
     * @throws IllegalAccessException
     *             if an error occurs in reflection while parsing the result.
     * @throws StorageException
     *             if a storage service error occurs.
     * @throws IOException
     *             if an error occurs while accessing the stream.
     * @throws JsonParseException
     *             if an error occurs while parsing the stream.
     */
    private static <T extends TableEntity, R> TableResult parseJsonEntity(final JsonParser parser,
            final Class<T> clazzType, HashMap<String, PropertyPair> classProperties, final EntityResolver<R> resolver,
            final TableRequestOptions options, final OperationContext opContext) throws JsonParseException,
            IOException, StorageException, InstantiationException, IllegalAccessException {
        final TableResult res = new TableResult();

        final HashMap<String, EntityProperty> properties = new HashMap<String, EntityProperty>();

        if (!parser.hasCurrentToken()) {
            parser.nextToken();
        }

        JsonUtilities.assertIsStartObjectJsonToken(parser);

        parser.nextToken();

        // get all metadata, if present
        while (parser.getCurrentName().startsWith(ODataConstants.ODATA_PREFIX)) {
            final String name = parser.getCurrentName().substring(ODataConstants.ODATA_PREFIX.length());

            // get the value token
            parser.nextToken();

            if (name.equals(ODataConstants.ETAG)) {
                String etag = parser.getValueAsString();
                res.setEtag(etag);
            }

            // get the key token
            parser.nextToken();
        }

        if (resolver == null && clazzType == null) {
            return res;
        }

        // get object properties
        while (parser.getCurrentToken() != JsonToken.END_OBJECT) {
            String key = Constants.EMPTY_STRING;
            String val = Constants.EMPTY_STRING;
            EdmType edmType = null;

            // checks if this property is preceded by an OData property type annotation
            if (options.getTablePayloadFormat() != TablePayloadFormat.JsonNoMetadata
                    && parser.getCurrentName().endsWith(ODataConstants.ODATA_TYPE_SUFFIX)) {
                parser.nextToken();
                edmType = EdmType.parse(parser.getValueAsString());

                parser.nextValue();
                key = parser.getCurrentName();
                val = parser.getValueAsString();
            }
            else {
                key = parser.getCurrentName();

                parser.nextToken();
                val = parser.getValueAsString();
                edmType = evaluateEdmType(parser.getCurrentToken(), parser.getValueAsString());
            }

            final EntityProperty newProp = new EntityProperty(val, edmType);
            newProp.setDateBackwardCompatibility(options.getDateBackwardCompatibility());
            properties.put(key, newProp);

            parser.nextToken();
        }

        String partitionKey = null;
        String rowKey = null;
        Date timestamp = null;
        String etag = null;

        // Remove core properties from map and set individually
        EntityProperty tempProp = properties.remove(TableConstants.PARTITION_KEY);
        if (tempProp != null) {
            partitionKey = tempProp.getValueAsString();
        }

        tempProp = properties.remove(TableConstants.ROW_KEY);
        if (tempProp != null) {
            rowKey = tempProp.getValueAsString();
        }

        tempProp = properties.remove(TableConstants.TIMESTAMP);
        if (tempProp != null) {
            tempProp.setDateBackwardCompatibility(false);
            timestamp = tempProp.getValueAsDate();

            if (res.getEtag() == null) {
                etag = getETagFromTimestamp(tempProp.getValueAsString());
                res.setEtag(etag);
            }
        }

        // do further processing for type if JsonNoMetdata by inferring type information via resolver or clazzType
        if (options.getTablePayloadFormat() == TablePayloadFormat.JsonNoMetadata
                && (options.getPropertyResolver() != null || clazzType != null)) {
            if (options.getPropertyResolver() != null) {
                for (final Entry<String, EntityProperty> p : properties.entrySet()) {
                    final String key = p.getKey();
                    final String value = p.getValue().getValueAsString();
                    EdmType edmType;

                    // try to use the property resolver to get the type
                    try {
                        edmType = options.getPropertyResolver().propertyResolver(partitionKey, rowKey, key, value);
                    }
                    catch (Exception e) {
                        throw new StorageException(StorageErrorCodeStrings.INTERNAL_ERROR, SR.CUSTOM_RESOLVER_THREW,
                                Constants.HeaderConstants.HTTP_UNUSED_306, null, e);
                    }

                    // try to create a new entity property using the returned type
                    try {
                        final EntityProperty newProp = new EntityProperty(value, edmType);
                        newProp.setDateBackwardCompatibility(options.getDateBackwardCompatibility());
                        properties.put(p.getKey(), newProp);
                    }
                    catch (IllegalArgumentException e) {
                        throw new StorageException(StorageErrorCodeStrings.INVALID_TYPE, String.format(
                                SR.FAILED_TO_PARSE_PROPERTY, key, value, edmType),
                                Constants.HeaderConstants.HTTP_UNUSED_306, null, e);
                    }
                }
            }
            else if (clazzType != null) {
                if (classProperties == null) {
                    classProperties = PropertyPair.generatePropertyPairs(clazzType);
                }
                for (final Entry<String, EntityProperty> p : properties.entrySet()) {
                    PropertyPair propPair = classProperties.get(p.getKey());
                    if (propPair != null) {
                        final EntityProperty newProp = new EntityProperty(p.getValue().getValueAsString(),
                                propPair.type);
                        newProp.setDateBackwardCompatibility(options.getDateBackwardCompatibility());
                        properties.put(p.getKey(), newProp);
                    }
                }
            }
        }

        // set the result properties, now that they are appropriately parsed
        res.setProperties(properties);

        // use resolver if provided, else create entity based on clazz type
        if (resolver != null) {
            res.setResult(resolver.resolve(partitionKey, rowKey, timestamp, res.getProperties(), res.getEtag()));
        }
        else if (clazzType != null) {
            // Generate new entity and return
            final T entity = clazzType.newInstance();
            entity.setEtag(res.getEtag());

            entity.setPartitionKey(partitionKey);
            entity.setRowKey(rowKey);
            entity.setTimestamp(timestamp);

            entity.readEntity(res.getProperties(), opContext);

            res.setResult(entity);
        }

        return res;
    }

    private static String getETagFromTimestamp(String timestampString) throws UnsupportedEncodingException {
        timestampString = URLEncoder.encode(timestampString, Constants.UTF8_CHARSET);
        return "W/\"datetime'" + timestampString + "'\"";
    }

    private static EdmType evaluateEdmType(JsonToken token, String value) {
        EdmType edmType = null;

        if (token == JsonToken.VALUE_FALSE || token == JsonToken.VALUE_TRUE) {
            edmType = EdmType.BOOLEAN;
        }
        else if (token == JsonToken.VALUE_NUMBER_FLOAT) {
            edmType = EdmType.DOUBLE;
        }
        else if (token == JsonToken.VALUE_NUMBER_INT) {
            edmType = EdmType.INT32;
        }
        else {
            edmType = EdmType.STRING;
        }

        return edmType;
    }

    private static JsonParser createJsonParserFromStream(final InputStream streamRef) throws JsonParseException,
            IOException {
        JsonParser parser = jsonFactory.createParser(streamRef);

        return parser
                // allows handling of infinity, -infinity, and NaN for Doubles
                .enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS)
                // don't close the stream and allow it to be drained completely
                // in ExecutionEngine to improve socket reuse
                .disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
    }
}


/**
 * 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.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

/**
 * RESERVED FOR INTERNAL USE. This is a Version 2 Canonicalization strategy conforming to the PDC 2009-09-19
 * specification
 */
abstract class Canonicalizer {

    /**
     * The expected length for the canonicalized string when SharedKeyFull is used to sign requests.
     */
    private static final int ExpectedBlobQueueCanonicalizedStringLength = 300;

    /**
     * The expected length for the canonicalized string when SharedKeyFull is used to sign table requests.
     */
    private static final int ExpectedTableCanonicalizedStringLength = 200;

    private static final Pattern CRLF  = Pattern.compile("\r\n", Pattern.LITERAL);

    /**
     * Add x-ms- prefixed headers in a fixed order.
     *
     * @param conn
     *            the HttpURLConnection for the operation
     * @param canonicalizedString
     *            the canonicalized string to add the canonicalized headerst to.
     */
    private static void addCanonicalizedHeaders(final HttpURLConnection conn, final StringBuilder canonicalizedString) {
        // Look for header names that start with
        // HeaderNames.PrefixForStorageHeader
        // Then sort them in case-insensitive manner.

        final Map<String, List<String>> headers = conn.getRequestProperties();
        final ArrayList<String> httpStorageHeaderNameArray = new ArrayList<String>();

        for (final String key : headers.keySet()) {
            if (key.toLowerCase(Utility.LOCALE_US).startsWith(Constants.PREFIX_FOR_STORAGE_HEADER)) {
                httpStorageHeaderNameArray.add(key.toLowerCase(Utility.LOCALE_US));
            }
        }

        Collections.sort(httpStorageHeaderNameArray);

        // Now go through each header's values in the sorted order and append
        // them to the canonicalized string.
        for (final String key : httpStorageHeaderNameArray) {
            final StringBuilder canonicalizedElement = new StringBuilder(key);
            String delimiter = ":";
            final ArrayList<String> values = getHeaderValues(headers, key);

            boolean appendCanonicalizedElement = false;
            // Go through values, unfold them, and then append them to the
            // canonicalized element string.
            for (final String value : values) {
                if (value != null) {
                    appendCanonicalizedElement = true;
                }

                // Unfolding is simply removal of CRLF.
                final String unfoldedValue = CRLF.matcher(value)
                    .replaceAll(Matcher.quoteReplacement(Constants.EMPTY_STRING));

                // Append it to the canonicalized element string.
                canonicalizedElement.append(delimiter);
                canonicalizedElement.append(unfoldedValue);
                delimiter = ",";
            }

            // Now, add this canonicalized element to the canonicalized header
            // string.
            if (appendCanonicalizedElement) {
                appendCanonicalizedElement(canonicalizedString, canonicalizedElement.toString());
            }
        }
    }

    /**
     * Append a string to a string builder with a newline constant
     *
     * @param builder
     *            the StringBuilder object
     * @param element
     *            the string to append.
     */
    protected static void appendCanonicalizedElement(final StringBuilder builder, final String element) {
        builder.append("\n");
        builder.append(element);
    }

    /**
     * Constructs a canonicalized string from the request's headers that will be used to construct the signature string
     * for signing a Blob or Queue service request under the Shared Key Full authentication scheme.
     *
     * @param address
     *            the request URI
     * @param accountName
     *            the account name associated with the request
     * @param method
     *            the verb to be used for the HTTP request.
     * @param contentType
     *            the content type of the HTTP request.
     * @param contentLength
     *            the length of the content written to the outputstream in bytes, -1 if unknown
     * @param date
     *            the date/time specification for the HTTP request
     * @param conn
     *            the HttpURLConnection for the operation.
     * @return A canonicalized string.
     * @throws StorageException
     */
    protected static String canonicalizeHttpRequest(final java.net.URL address, final String accountName,
            final String method, final String contentType, final long contentLength, final String date,
            final HttpURLConnection conn) throws StorageException {

        // The first element should be the Method of the request.
        // I.e. GET, POST, PUT, or HEAD.
        final StringBuilder canonicalizedString = new StringBuilder(ExpectedBlobQueueCanonicalizedStringLength);
        canonicalizedString.append(conn.getRequestMethod());

        // The next elements are
        // If any element is missing it may be empty.
        appendCanonicalizedElement(canonicalizedString,
                Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.CONTENT_ENCODING));
        appendCanonicalizedElement(canonicalizedString,
                Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.CONTENT_LANGUAGE));
        appendCanonicalizedElement(canonicalizedString,
                contentLength <= 0 ? Constants.EMPTY_STRING : String.valueOf(contentLength));
        appendCanonicalizedElement(canonicalizedString,
                Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.CONTENT_MD5));
        appendCanonicalizedElement(canonicalizedString, contentType != null ? contentType : Constants.EMPTY_STRING);

        final String dateString = Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.DATE);
        // If x-ms-date header exists, Date should be empty string
        appendCanonicalizedElement(canonicalizedString, dateString.equals(Constants.EMPTY_STRING) ? date
                : Constants.EMPTY_STRING);

        appendCanonicalizedElement(canonicalizedString,
                Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.IF_MODIFIED_SINCE));
        appendCanonicalizedElement(canonicalizedString,
                Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.IF_MATCH));
        appendCanonicalizedElement(canonicalizedString,
                Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.IF_NONE_MATCH));
        appendCanonicalizedElement(canonicalizedString,
                Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.IF_UNMODIFIED_SINCE));
        appendCanonicalizedElement(canonicalizedString,
                Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.RANGE));

        addCanonicalizedHeaders(conn, canonicalizedString);

        appendCanonicalizedElement(canonicalizedString, getCanonicalizedResource(address, accountName));

        return canonicalizedString.toString();
    }

    /**
     * Constructs a canonicalized string that will be used to construct the signature string
     * for signing a Table service request under the Shared Key authentication scheme.
     *
     * @param address
     *            the request URI
     * @param accountName
     *            the account name associated with the request
     * @param method
     *            the verb to be used for the HTTP request.
     * @param contentType
     *            the content type of the HTTP request.
     * @param contentLength
     *            the length of the content written to the outputstream in bytes, -1 if unknown
     * @param date
     *            the date/time specification for the HTTP request
     * @param conn
     *            the HttpURLConnection for the operation.
     * @return A canonicalized string.
     * @throws StorageException
     */
    protected static String canonicalizeTableHttpRequest(final java.net.URL address, final String accountName,
            final String method, final String contentType, final long contentLength, final String date,
            final HttpURLConnection conn) throws StorageException {
        // The first element should be the Method of the request.
        // I.e. GET, POST, PUT, or HEAD.
        final StringBuilder canonicalizedString = new StringBuilder(ExpectedTableCanonicalizedStringLength);
        canonicalizedString.append(conn.getRequestMethod());

        // The second element should be the MD5 value.
        // This is optional and may be empty.
        final String httpContentMD5Value = Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.CONTENT_MD5);
        appendCanonicalizedElement(canonicalizedString, httpContentMD5Value);

        // The third element should be the content type.
        appendCanonicalizedElement(canonicalizedString, contentType);

        // The fourth element should be the request date.
        // See if there's an storage date header.
        // If there's one, then don't use the date header.

        final String dateString = Utility.getStandardHeaderValue(conn, Constants.HeaderConstants.DATE);
        // If x-ms-date header exists, Date should be that value.
        appendCanonicalizedElement(canonicalizedString, dateString.equals(Constants.EMPTY_STRING) ? date : dateString);

        appendCanonicalizedElement(canonicalizedString, getCanonicalizedResourceLite(address, accountName));

        return canonicalizedString.toString();
    }

    /**
     * Gets the canonicalized resource string for a Blob or Queue service request under the Shared Key Lite
     * authentication scheme.
     *
     * @param address
     *            the resource URI.
     * @param accountName
     *            the account name for the request.
     * @return the canonicalized resource string.
     * @throws StorageException
     */
    protected static String getCanonicalizedResource(final java.net.URL address, final String accountName)
            throws StorageException {
        // Resource path
        final StringBuilder resourcepath = new StringBuilder("/");
        resourcepath.append(accountName);

        // Note that AbsolutePath starts with a '/'.
        resourcepath.append(address.getPath());
        final StringBuilder canonicalizedResource = new StringBuilder(resourcepath.toString());

        // query parameters
        if (address.getQuery() == null  || !address.getQuery().contains("=")) {
            //no query params.
            return canonicalizedResource.toString();
        }

        final Map<String, String[]> queryVariables = PathUtility.parseQueryString(address.getQuery());

        final Map<String, String> lowercasedKeyNameValue = new HashMap<String, String>();

        for (final Entry<String, String[]> entry : queryVariables.entrySet()) {
            // sort the value and organize it as comma separated values
            final List<String> sortedValues = Arrays.asList(entry.getValue());
            Collections.sort(sortedValues);

            final StringBuilder stringValue = new StringBuilder();

            for (final String value : sortedValues) {
                if (stringValue.length() > 0) {
                    stringValue.append(",");
                }

                stringValue.append(value);
            }

            // key turns out to be null for ?a&b&c&d
            lowercasedKeyNameValue.put((entry.getKey()) == null ? null :
                entry.getKey().toLowerCase(Utility.LOCALE_US), stringValue.toString());
        }

        final ArrayList<String> sortedKeys = new ArrayList<String>(lowercasedKeyNameValue.keySet());

        Collections.sort(sortedKeys);

        for (final String key : sortedKeys) {
            final StringBuilder queryParamString = new StringBuilder();

            queryParamString.append(key);
            queryParamString.append(":");
            queryParamString.append(lowercasedKeyNameValue.get(key));

            appendCanonicalizedElement(canonicalizedResource, queryParamString.toString());
        }

        return canonicalizedResource.toString();
    }

    /**
     * Gets the canonicalized resource string for a Blob or Queue service request under the Shared Key Lite
     * authentication scheme.
     *
     * @param address
     *            the resource URI.
     * @param accountName
     *            the account name for the request.
     * @return the canonicalized resource string.
     * @throws StorageException
     */
    protected static String getCanonicalizedResourceLite(final java.net.URL address, final String accountName)
            throws StorageException {
        // Resource path
        final StringBuilder resourcepath = new StringBuilder("/");
        resourcepath.append(accountName);

        // Note that AbsolutePath starts with a '/'.
        resourcepath.append(address.getPath());
        final StringBuilder canonicalizedResource = new StringBuilder(resourcepath.toString());

        // query parameters
        final Map<String, String[]> queryVariables = PathUtility.parseQueryString(address.getQuery());

        final String[] compVals = queryVariables.get("comp");

        if (compVals != null) {

            final List<String> sortedValues = Arrays.asList(compVals);
            Collections.sort(sortedValues);

            canonicalizedResource.append("?comp=");

            final StringBuilder stringValue = new StringBuilder();
            for (final String value : sortedValues) {
                if (stringValue.length() > 0) {
                    stringValue.append(",");
                }
                stringValue.append(value);
            }

            canonicalizedResource.append(stringValue);
        }

        return canonicalizedResource.toString();
    }

    /**
     * Gets all the values for the given header in the one to many map, performs a trimStart() on each return value
     *
     * @param headers
     *            a one to many map of key / values representing the header values for the connection.
     * @param headerName
     *            the name of the header to lookup
     * @return an ArrayList<String> of all trimmed values corresponding to the requested headerName. This may be empty
     *         if the header is not found.
     */
    private static ArrayList<String> getHeaderValues(final Map<String, List<String>> headers, final String headerName) {

        final ArrayList<String> arrayOfValues = new ArrayList<String>();
        List<String> values = null;

        for (final Entry<String, List<String>> entry : headers.entrySet()) {
            if (entry.getKey().toLowerCase(Utility.LOCALE_US).equals(headerName)) {
                values = entry.getValue();
                break;
            }
        }
        if (values != null) {
            for (final String value : values) {
                // canonicalization formula requires the string to be left
                // trimmed.
                arrayOfValues.add(Utility.trimStart(value));
            }
        }
        return arrayOfValues;
    }

    /**
     * Constructs a canonicalized string for signing a request.
     *
     * @param conn
     *            the HttpURLConnection to canonicalize
     * @param accountName
     *            the account name associated with the request
     * @param contentLength
     *            the length of the content written to the outputstream in bytes, -1 if unknown
     * @return a canonicalized string.
     */
    protected abstract String canonicalize(HttpURLConnection conn, String accountName, Long contentLength)
            throws StorageException;
}


/**
 * 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.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;

import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.StorageCredentials;
import com.microsoft.azure.storage.StorageCredentialsAccountAndKey;
import com.microsoft.azure.storage.StorageCredentialsSharedAccessSignature;
import com.microsoft.azure.storage.StorageException;

/**
 * RESERVED FOR INTERNAL USE. A helper method for StorageCredentials.
 */
public final class StorageCredentialsHelper {

    /**
     *  RESERVED, for internal use only. Gets a value indicating whether a
     *  request can be signed under the Shared Key authentication scheme using
     *  the specified credentials.

     *  @return true if a request can be signed with these
     *  credentials; otherwise, false
     */
    public static boolean canCredentialsSignRequest(final StorageCredentials creds) {
        return creds.getClass().equals(StorageCredentialsAccountAndKey.class);
    }

    /**
     *  RESERVED, for internal use only. Gets a value indicating whether a
     *  client can be generated under the Shared Key or Shared Access Signature
     *  authentication schemes using the specified credentials.
     *  @return true if a client can be generated with these
     *  credentials; otherwise, false
     */
    public static boolean canCredentialsGenerateClient(final StorageCredentials creds) {
        return canCredentialsSignRequest(creds) || creds.getClass().equals(StorageCredentialsSharedAccessSignature.class);
    }

    /**
     * Computes a signature for the specified string using the HMAC-SHA256 algorithm.
     *
     * @param value
     *            The UTF-8-encoded string to sign.
     *
     * @return A String that contains the HMAC-SHA256-encoded signature.
     *
     * @throws InvalidKeyException
     *             If the key is not a valid Base64-encoded string.
     */
    public static synchronized String computeHmac256(final StorageCredentials creds, final String value) throws InvalidKeyException {
        if (creds.getClass().equals(StorageCredentialsAccountAndKey.class)) {
            byte[] utf8Bytes = null;
            try {
                utf8Bytes = value.getBytes(Constants.UTF8_CHARSET);
            }
            catch (final UnsupportedEncodingException e) {
                throw new IllegalArgumentException(e);
            }
            return Base64.encode(((StorageCredentialsAccountAndKey) creds).getHmac256().doFinal(utf8Bytes));
        }
        else {
            return null;
        }
    }

    /**
     * Signs a request using the specified operation context under the Shared Key authentication scheme.
     *
     * @param request
     *            An HttpURLConnection object that represents the request to sign.
     * @param contentLength
     *            The length of the content written to the output stream. If unknown, specify -1.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     *
     * @throws InvalidKeyException
     *             If the given key is invalid.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static void signBlobQueueAndFileRequest(final StorageCredentials creds,
            final java.net.HttpURLConnection request, final long contentLength, OperationContext opContext)
            throws InvalidKeyException, StorageException {

        if (creds.getClass().equals(StorageCredentialsAccountAndKey.class)) {
            opContext = opContext == null ? new OperationContext() : opContext;
            request.setRequestProperty(Constants.HeaderConstants.DATE, Utility.getGMTTime());
            final Canonicalizer canonicalizer = CanonicalizerFactory.getBlobQueueFileCanonicalizer(request);

            final String stringToSign = canonicalizer.canonicalize(request, creds.getAccountName(), contentLength);

            final String computedBase64Signature = StorageCredentialsHelper.computeHmac256(creds, stringToSign);

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

            request.setRequestProperty(Constants.HeaderConstants.AUTHORIZATION,
                    String.format("%s %s:%s", "SharedKey", creds.getAccountName(), computedBase64Signature));
        }
    }

    /**
     * Signs a request using the specified operation context under the Shared Key authentication scheme.
     *
     * @param request
     *            An HttpURLConnection object that represents the request to sign.
     * @param contentLength
     *            The length of the content written to the output stream. If unknown, specify -1.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     *
     * @throws InvalidKeyException
     *             If the given key is invalid.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static void signTableRequest(final StorageCredentials creds, final java.net.HttpURLConnection request,
            final long contentLength, OperationContext opContext) throws InvalidKeyException, StorageException {
        if (creds.getClass().equals(StorageCredentialsAccountAndKey.class)) {
            opContext = opContext == null ? new OperationContext() : opContext;
            request.setRequestProperty(Constants.HeaderConstants.DATE, Utility.getGMTTime());

            final Canonicalizer canonicalizer = CanonicalizerFactory.getTableCanonicalizer(request);

            final String stringToSign = canonicalizer.canonicalize(request, creds.getAccountName(), contentLength);

            final String computedBase64Signature = StorageCredentialsHelper.computeHmac256(creds, stringToSign);

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

            request.setRequestProperty(Constants.HeaderConstants.AUTHORIZATION,
                    String.format("%s %s:%s", "SharedKey", creds.getAccountName(), computedBase64Signature));
        }
    }

    /**
     * A private default constructor. All methods of this class are static so no instances of it should ever be created.
     */
    private StorageCredentialsHelper() {
        //No op
    }
}