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

import java.net.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;

/**
 * Represents the current logical operation. A logical operation may have a one-to-many relationship with
 * multiple individual physical requests.
 */
public final class OperationContext {

    /**
     * The default log level, or null if disabled. The default can be overridden to turn on logging for an individual
     * operation context instance by using setLoggingEnabled.
     */
    private static Integer defaultLogLevel;

    /**
     * Indicates whether the client library should produce log entries by default. The default can be overridden to
     * enable logging for an individual operation context instance by using {@link #setLoggingEnabled}.
     */
    private static boolean enableLoggingByDefault = false;

    /**
     * Indicates whether the client library should use a proxy by default. The default can be overridden to
     * enable proxy for an individual operation context instance by using {@link #setProxy}.
     */
    private static Proxy proxyDefault;

    /**
     * Represents a proxy to be used when making a request.
     */
    private Proxy proxy;

    /**
     * Represents the operation latency, in milliseconds, from the client's perspective. This may include any potential
     * retries.
     */
    private long clientTimeInMs;

    /**
     * The UUID representing the client side trace ID.
     */
    private String clientRequestID;

    /**
     * The log level for a given operation context, null if disabled.
     */
    private Integer logLevel;

    /**
     * Represents request results, in the form of an ArrayList object that contains the
     * {@link RequestResult} objects, for each physical request that is made.
     */
    private final ArrayList<RequestResult> requestResults;

    /**
     * Represents additional headers on the request, for example, for proxy or logging information.
     */
    private HashMap<String, String> userHeaders;

    /**
     * Represents an event that is triggered before sending a
     * request.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see SendingRequestEvent
     */
    private static StorageEventMultiCaster<SendingRequestEvent, StorageEvent<SendingRequestEvent>> globalSendingRequestEventHandler = new StorageEventMultiCaster<SendingRequestEvent, StorageEvent<SendingRequestEvent>>();

    /**
     * Represents an event that is triggered when a response is received from the storage service while processing a request
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see ResponseReceivedEvent
     */
    private static StorageEventMultiCaster<ResponseReceivedEvent, StorageEvent<ResponseReceivedEvent>> globalResponseReceivedEventHandler = new StorageEventMultiCaster<ResponseReceivedEvent, StorageEvent<ResponseReceivedEvent>>();

    /**
     * Represents an event that is triggered when a network error occurs before the HTTP response status and headers are received.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see ErrorReceivingResponseEvent
     */
    private static StorageEventMultiCaster<ErrorReceivingResponseEvent, StorageEvent<ErrorReceivingResponseEvent>> globalErrorReceivingResponseEventHandler = new StorageEventMultiCaster<ErrorReceivingResponseEvent, StorageEvent<ErrorReceivingResponseEvent>>();

    /**
     * Represents an event that is triggered when a response received from the service is fully processed.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see RequestCompletedEvent
     */
    private static StorageEventMultiCaster<RequestCompletedEvent, StorageEvent<RequestCompletedEvent>> globalRequestCompletedEventHandler = new StorageEventMultiCaster<RequestCompletedEvent, StorageEvent<RequestCompletedEvent>>();

    /**
     * Represents an event that is triggered when a request is retried.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see RetryingEvent
     */
    private static StorageEventMultiCaster<RetryingEvent, StorageEvent<RetryingEvent>> globalRetryingEventHandler = new StorageEventMultiCaster<RetryingEvent, StorageEvent<RetryingEvent>>();

    /**
     * Represents an event that is triggered before sending a request.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see SendingRequestEvent
     */
    private StorageEventMultiCaster<SendingRequestEvent, StorageEvent<SendingRequestEvent>> sendingRequestEventHandler = new StorageEventMultiCaster<SendingRequestEvent, StorageEvent<SendingRequestEvent>>();

    /**
     * Represents an event that is triggered when a response is received from the storage service while processing a
     * request.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see ResponseReceivedEvent
     */
    private StorageEventMultiCaster<ResponseReceivedEvent, StorageEvent<ResponseReceivedEvent>> responseReceivedEventHandler = new StorageEventMultiCaster<ResponseReceivedEvent, StorageEvent<ResponseReceivedEvent>>();

    /**
     * Represents an event that is triggered when a network error occurs before the HTTP response status and headers are received.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see ErrorReceivingResponseEvent
     */
    private StorageEventMultiCaster<ErrorReceivingResponseEvent, StorageEvent<ErrorReceivingResponseEvent>> errorReceivingResponseEventHandler = new StorageEventMultiCaster<ErrorReceivingResponseEvent, StorageEvent<ErrorReceivingResponseEvent>>();

    /**
     * Represents an event that is triggered when a response received from the service is fully processed.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see RequestCompletedEvent
     */
    private StorageEventMultiCaster<RequestCompletedEvent, StorageEvent<RequestCompletedEvent>> requestCompletedEventHandler = new StorageEventMultiCaster<RequestCompletedEvent, StorageEvent<RequestCompletedEvent>>();

    /**
     * Represents an event that is triggered when a response is received from the storage service while processing a
     * request.
     *
     * @see StorageEvent
     * @see StorageEventMultiCaster
     * @see RetryingEvent
     */
    private StorageEventMultiCaster<RetryingEvent, StorageEvent<RetryingEvent>> retryingEventHandler = new StorageEventMultiCaster<RetryingEvent, StorageEvent<RetryingEvent>>();

    /**
     * Creates an instance of the OperationContext class.
     */
    public OperationContext() {
        this.clientRequestID = UUID.randomUUID().toString();
        this.requestResults = new ArrayList<RequestResult>();
    }

    /**
     * Gets the client side trace ID.
     *
     * @return A String</cod> which represents the client request ID.
     */
    public String getClientRequestID() {
        return this.clientRequestID;
    }

    /**
     * Gets the operation latency, in milliseconds, from the client's perspective. This may include any potential
     * retries.
     *
     * @return A long which contains the client latency time in milliseconds.
     */
    public long getClientTimeInMs() {
        return this.clientTimeInMs;
    }

    /**
     * Gets the last request result encountered for the operation.
     *
     * @return A {@link RequestResult} object which represents the last request result.
     */
    public synchronized RequestResult getLastResult() {
        if (this.requestResults == null || this.requestResults.size() == 0) {
            return null;
        }
        else {
            return this.requestResults.get(this.requestResults.size() - 1);
        }
    }

    /**
     * Gets a proxy which will be used when making a request. Default is null. To set a proxy to use by
     * default, use {@link #setDefaultProxy}
     *
     * @return A {@link java.net.Proxy} to use when making a request.
     */
    public Proxy getProxy() {
        return this.proxy;
    }

    /**
     * Gets any additional headers for the request, for example, for proxy or logging information.
     *
     * @return A java.util.HashMap which contains the the user headers for the request.
     */
    public HashMap<String, String> getUserHeaders() {
        return this.userHeaders;
    }

    /**
     * Returns the set of request results that the current operation has created.
     *
     * @return An ArrayList object that contains {@link RequestResult} objects that represent
     *         the request results created by the current operation.
     */
    public ArrayList<RequestResult> getRequestResults() {
        return this.requestResults;
    }

    /**
     * Reserved for internal use. Appends a {@link RequestResult} object to the internal collection in a synchronized
     * manner.
     *
     * @param requestResult
     *        A {@link RequestResult} to append.
     */
    public synchronized void appendRequestResult(RequestResult requestResult) {
        this.requestResults.add(requestResult);
    }

    /**
     * Gets an Integer indicating at what level the client library should produce log entries by default. The
     * default can be overridden to turn on logging for an individual operation context instance by using
     * {@link #setLogLevel(Integer)}.
     *
     * @return the android.util.Log level to log at, or null if disabled
     * @see Android Log Constants
     */
    public static Integer getDefaultLogLevel() {
        return defaultLogLevel;
    }

    /**
     * Gets a global event multi-caster that is triggered before sending a request. It allows event listeners to be
     * dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the globalSendingRequestEventHandler.
     */
    public static StorageEventMultiCaster<SendingRequestEvent, StorageEvent<SendingRequestEvent>> getGlobalSendingRequestEventHandler() {
        return OperationContext.globalSendingRequestEventHandler;
    }

    /**
     * Gets a global event multi-caster that is triggered when a response is received. It allows event listeners to be
     * dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the globalResponseReceivedEventHandler.
     */
    public static StorageEventMultiCaster<ResponseReceivedEvent, StorageEvent<ResponseReceivedEvent>> getGlobalResponseReceivedEventHandler() {
        return OperationContext.globalResponseReceivedEventHandler;
    }

    /**
     * Gets a global event multi-caster that is triggered when a network error occurs before the HTTP response status and headers are received.
     * It allows event listeners to be dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the globabErrorReceivingResponseEventHandler.
     */
    public static StorageEventMultiCaster<ErrorReceivingResponseEvent, StorageEvent<ErrorReceivingResponseEvent>> getGlobalErrorReceivingResponseEventHandler() {
        return OperationContext.globalErrorReceivingResponseEventHandler;
    }

    /**
     * Gets a global event multi-caster that is triggered when a request is completed. It allows event listeners to be
     * dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the globalRequestCompletedEventHandler.
     */
    public static StorageEventMultiCaster<RequestCompletedEvent, StorageEvent<RequestCompletedEvent>> getGlobalRequestCompletedEventHandler() {
        return OperationContext.globalRequestCompletedEventHandler;
    }

    /**
     * Gets a global event multi-caster that is triggered when a request is retried. It allows event listeners to be
     * dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the globalRetryingEventHandler.
     */
    public static StorageEventMultiCaster<RetryingEvent, StorageEvent<RetryingEvent>> getGlobalRetryingEventHandler() {
        return OperationContext.globalRetryingEventHandler;
    }

    /**
     * Gets the log level for this operation context.
     *
     * @return the android.util.Log level to log at, or null if disabled
     * @see Android Log Constants
     */
    public Integer getLogLevel() {
        return this.logLevel;
    }

    /**
     * Gets an event multi-caster that is triggered before sending a request. It allows event listeners to be
     * dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the sendingRequestEventHandler.
     */
    public StorageEventMultiCaster<SendingRequestEvent, StorageEvent<SendingRequestEvent>> getSendingRequestEventHandler() {
        return this.sendingRequestEventHandler;
    }

    /**
     * Gets an event multi-caster that is triggered when a response is received. It allows event listeners to be
     * dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the responseReceivedEventHandler.
     */
    public StorageEventMultiCaster<ResponseReceivedEvent, StorageEvent<ResponseReceivedEvent>> getResponseReceivedEventHandler() {
        return this.responseReceivedEventHandler;
    }

    /**
     * Gets an event multi-caster that is triggered when a network error occurs before the HTTP response status and headers are received.
     * It allows event listeners to be dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the errorReceivingResponseEventHandler.
     */
    public StorageEventMultiCaster<ErrorReceivingResponseEvent, StorageEvent<ErrorReceivingResponseEvent>> getErrorReceivingResponseEventHandler() {
        return this.errorReceivingResponseEventHandler;
    }

    /**
     * Gets an event multi-caster that is triggered when a request is completed. It allows event listeners to be
     * dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the requestCompletedEventHandler.
     */
    public StorageEventMultiCaster<RequestCompletedEvent, StorageEvent<RequestCompletedEvent>> getRequestCompletedEventHandler() {
        return this.requestCompletedEventHandler;
    }

    /**
     * Gets an event multi-caster that is triggered when a request is retried. It allows event listeners to be
     * dynamically added and removed.
     *
     * @return A {@link StorageEventMultiCaster} object for the retryingEventHandler.
     */
    public StorageEventMultiCaster<RetryingEvent, StorageEvent<RetryingEvent>> getRetryingEventHandler() {
        return this.retryingEventHandler;
    }

    /**
     * Reserved for internal use. Initializes the OperationContext in order to begin processing a
     * new operation. All operation specific information is erased.
     */
    public void initialize() {
        this.setClientTimeInMs(0);
        this.requestResults.clear();
    }

    /**
     * Set the client side trace ID.
     *
     * @param clientRequestID
     *            A String which contains the client request ID to set.
     */
    public void setClientRequestID(final String clientRequestID) {
        this.clientRequestID = clientRequestID;
    }

    /**
     * Reserved for internal use. Represents the operation latency, in milliseconds, from the client's perspective. This
     * may include any potential retries.
     *
     * @param clientTimeInMs
     *            A long which contains the client operation latency in milliseconds.
     */
    public void setClientTimeInMs(final long clientTimeInMs) {
        this.clientTimeInMs = clientTimeInMs;
    }

    /**
     * Specifies an Integer indicating at what level the client library should produce log entries by default. The
     * default can be overridden to turn on logging for an individual operation context instance by using
     * {@link #setLogLevel(Integer)}.
     *
     * @param defaultLogLevel
     *            the android.util.Log level to log at, or null if disabled
     * @see Android Log Constants
     */
    public static void setDefaultLogLevel(Integer defaultLogLevel) {
        OperationContext.defaultLogLevel = defaultLogLevel;
    }

    /**
     * Sets a proxy which will be used when making a request. Default is null. To set a proxy to use by
     * default, use {@link #setDefaultProxy}
     *
     * @param proxy
     *            A {@link java.net.Proxy} to use when making a request.
     */
    public void setProxy(Proxy proxy) {
        this.proxy = proxy;
    }

    /**
     * Sets any additional headers for the request, for example, for proxy or logging information.
     *
     * @param userHeaders
     *            A java.util.HashMap which contains any additional headers to set.
     */
    public void setUserHeaders(final HashMap<String, String> userHeaders) {
        this.userHeaders = userHeaders;
    }

    /**
     * Sets a global event multi-caster that is triggered before sending a request.
     *
     * @param globalSendingRequestEventHandler
     *            The {@link StorageEventMultiCaster} object to set for the
     *            globalSendingRequestEventHandler.
     */
    public static void setGlobalSendingRequestEventHandler(
            final StorageEventMultiCaster<SendingRequestEvent, StorageEvent<SendingRequestEvent>> globalSendingRequestEventHandler) {
        OperationContext.globalSendingRequestEventHandler = globalSendingRequestEventHandler;
    }

    /**
     * Sets a global event multi-caster that is triggered when a response is received.
     *
     * @param globalResponseReceivedEventHandler
     *            The {@link StorageEventMultiCaster} object to set for the
     *            globalResponseReceivedEventHandler.
     */
    public static void setGlobalResponseReceivedEventHandler(
            final StorageEventMultiCaster<ResponseReceivedEvent, StorageEvent<ResponseReceivedEvent>> globalResponseReceivedEventHandler) {
        OperationContext.globalResponseReceivedEventHandler = globalResponseReceivedEventHandler;
    }

    /**
     * Sets a global event multi-caster that is triggered when a network error occurs before the HTTP response status and headers are received.
     *
     * @param globalErrorReceivingResponseEventHandler
     *        The {@link StorageEventMultiCaster} object to set for the globalErrorReceivingResponseEventHandler.
     */
    public static void setGlobalErrorReceivingResponseEventHandler(
            final StorageEventMultiCaster<ErrorReceivingResponseEvent, StorageEvent<ErrorReceivingResponseEvent>> globalErrorReceivingResponseEventHandler) {
        OperationContext.globalErrorReceivingResponseEventHandler = globalErrorReceivingResponseEventHandler;
    }

    /**
     * Sets a global event multi-caster that is triggered when a request is completed.
     *
     * @param globalRequestCompletedEventHandler
     *            The {@link StorageEventMultiCaster} object to set for the
     *            globalRequestCompletedEventHandler.
     */
    public static void setGlobalRequestCompletedEventHandler(
            final StorageEventMultiCaster<RequestCompletedEvent, StorageEvent<RequestCompletedEvent>> globalRequestCompletedEventHandler) {
        OperationContext.globalRequestCompletedEventHandler = globalRequestCompletedEventHandler;
    }

    /**
     * Sets a global event multi-caster that is triggered when a request is retried.
     *
     * @param globalRetryingEventHandler
     *            The {@link StorageEventMultiCaster} object to set for the globalRetryingEventHandler.
     */
    public static void setGlobalRetryingEventHandler(
            final StorageEventMultiCaster<RetryingEvent, StorageEvent<RetryingEvent>> globalRetryingEventHandler) {
        OperationContext.globalRetryingEventHandler = globalRetryingEventHandler;
    }

    /**
     * Sets the log level for this operation context.
     *
     * @param logLevel
     *            the android.util.Log level to log at, or null to disable
     * @see Android Log Constants
     */
    public void setLogLevel(Integer logLevel) {
        this.logLevel = logLevel;
    }

    /**
     * Sets an event multi-caster that is triggered before sending a request.
     *
     * @param sendingRequestEventHandler
     *            The {@link StorageEventMultiCaster} object to set for the sendingRequestEventHandler.
     */
    public void setSendingRequestEventHandler(
            final StorageEventMultiCaster<SendingRequestEvent, StorageEvent<SendingRequestEvent>> sendingRequestEventHandler) {
        this.sendingRequestEventHandler = sendingRequestEventHandler;
    }

    /**
     * Sets an event multi-caster that is triggered when a response is received.
     *
     * @param responseReceivedEventHandler
     *            The {@link StorageEventMultiCaster} object to set for the responseReceivedEventHandler.
     */
    public void setResponseReceivedEventHandler(
            final StorageEventMultiCaster<ResponseReceivedEvent, StorageEvent<ResponseReceivedEvent>> responseReceivedEventHandler) {
        this.responseReceivedEventHandler = responseReceivedEventHandler;
    }

    /**
     * Sets an event multi-caster that is triggered when a network error occurs before the HTTP response status and headers are received.
     *
     * @param errorReceivingResponseEventHandler
     *        The {@link StorageEventMultiCaster} object to set for the errorReceivingResponseEventHandler.
     */
    public void setErrorReceivingResponseEventHandler(
            final StorageEventMultiCaster<ErrorReceivingResponseEvent, StorageEvent<ErrorReceivingResponseEvent>> errorReceivingResponseEventHandler) {
        this.errorReceivingResponseEventHandler = errorReceivingResponseEventHandler;
    }

    /**
     * Sets an event multi-caster that is triggered when a request is completed.
     *
     * @param requestCompletedEventHandler
     *            The {@link StorageEventMultiCaster} object to set for the requestCompletedEventHandler.
     */
    public void setRequestCompletedEventHandler(
            final StorageEventMultiCaster<RequestCompletedEvent, StorageEvent<RequestCompletedEvent>> requestCompletedEventHandler) {
        this.requestCompletedEventHandler = requestCompletedEventHandler;
    }

    /**
     * Sets an event multi-caster that is triggered when a request is retried.
     *
     * @param retryingEventHandler
     *            The {@link StorageEventMultiCaster} object to set for the retryingEventHandler.
     */
    public void setRetryingEventHandler(
            final StorageEventMultiCaster<RetryingEvent, StorageEvent<RetryingEvent>> retryingEventHandler) {
        this.retryingEventHandler = retryingEventHandler;
    }

    /**
     * Gets the default proxy used by the client library if enabled. The default can be overridden
     * to enable a proxy for an individual operation context instance by using {@link #setProxy}.
     *
     * @return The default {@link java.net.Proxy} if set; otherwise null
     */
    public static Proxy getDefaultProxy() {
        return OperationContext.proxyDefault;
    }

    /**
     * Specifies the proxy the client library should use by default. The default can be overridden
     * to turn on a proxy for an individual operation context instance by using {@link #setProxy}.
     *
     * @param defaultProxy
     *        The {@link java.net.Proxy} to use by default, or null to not use a proxy.
     */
    public static void setDefaultProxy(Proxy defaultProxy) {
        OperationContext.proxyDefault = defaultProxy;
    }
}


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

import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.util.Map;

import com.microsoft.azure.storage.core.Base64;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.Utility;

/**
 * Represents a set of credentials used to authenticate access to a Microsoft Azure storage account. This is the base
 * class for the {@link StorageCredentialsAccountAndKey} and {@link StorageCredentialsSharedAccessSignature} classes.
 */
public abstract class StorageCredentials {

    /**
     * Tries to determine the storage credentials from a collection of name/value pairs.
     *
     * @param settings
     *            A Map object of the name/value pairs that represent the settings to use to configure
     *            the credentials.
     *

     *            Either include an account name with an account key (specifying values for
     *            {@link CloudStorageAccount#ACCOUNT_NAME_NAME} and {@link CloudStorageAccount#ACCOUNT_KEY_NAME} ), or a
     *            shared access signature (specifying a value for
     *            {@link CloudStorageAccount#SHARED_ACCESS_SIGNATURE_NAME} ). If you use an account name and account
     *            key, do not include a shared access signature, and vice versa.
     *
     * @return A {@link StorageCredentials} object representing the storage credentials determined from the name/value
     *         pairs.
     *
     * @throws InvalidKeyException
     *             If the key value specified for {@link CloudStorageAccount#ACCOUNT_KEY_NAME} is not a valid
     *             Base64-encoded string.
     */
    protected static StorageCredentials tryParseCredentials(final Map<String, String> settings)
            throws InvalidKeyException {
        final String accountName = settings.get(CloudStorageAccount.ACCOUNT_NAME_NAME) != null ?
                settings.get(CloudStorageAccount.ACCOUNT_NAME_NAME) : null;

        final String accountKey = settings.get(CloudStorageAccount.ACCOUNT_KEY_NAME) != null ?
                settings.get(CloudStorageAccount.ACCOUNT_KEY_NAME) : null;

        final String sasSignature = settings.get(CloudStorageAccount.SHARED_ACCESS_SIGNATURE_NAME) != null ?
                settings.get(CloudStorageAccount.SHARED_ACCESS_SIGNATURE_NAME) : null;

        if (accountName != null && accountKey != null && sasSignature == null) {
            if (Base64.validateIsBase64String(accountKey)) {
                return new StorageCredentialsAccountAndKey(accountName, accountKey);
            }
            else {
                throw new InvalidKeyException(SR.INVALID_KEY);
            }
        }
        if (accountKey == null && sasSignature != null) {
            return new StorageCredentialsSharedAccessSignature(sasSignature);
        }

        return null;
    }

    /**
     * Tries to determine the storage credentials from a connection string.
     *

     * The format for the connection string is in the pattern "keyname=value". Multiple key/value pairs can be
     * separated by a semi-colon, for example, "keyname1=value1;keyname2=value2". Either include an account name
     * with an account key or a shared access signature. If you use an account name and account key, do not include a
     * shared access signature, and vice versa.
     *

     * The same connection string can be used as for {@link CloudStorageAccount#parse(String)} but here only the account
     * name, account key, and sas key/value pairs will be examined.
     *
     * @param connectionString
     *            A String that contains the key/value pairs that represent the storage credentials.
     *
     * @return A {@link StorageCredentials} object representing the storage credentials determined from the connection
     *         string.
     *
     * @throws InvalidKeyException
     *             If the account key specified in connectionString is not valid.
     * @throws StorageException
     */
    public static StorageCredentials tryParseCredentials(final String connectionString)
            throws InvalidKeyException, StorageException {

        return tryParseCredentials(Utility.parseAccountString(connectionString));
    }

    /**
     * A boolean representing whether this StorageCredentials object only allows access via HTTPS.
     */
    private boolean httpsOnly = false;

    /**
     * Gets whether this StorageCredentials object only allows access via HTTPS.
     *
     * @return A boolean representing whether this StorageCredentials
     *         object only allows access via HTTPS.
     */
    public boolean isHttpsOnly() {
        return this.httpsOnly;
    }

    /**
     * Returns the associated account name for the credentials. This is null for anonymous and shared access signature
     * credentials.
     *
     * @return A String that represents the associated account name for the credentials
     */
    public String getAccountName() {
        return null;
    }

    /**
     * Sets whether this StorageCredentials object only allows access via HTTPS.
     * @param httpsOnly
     *            A boolean representing whether this StorageCredentials
     *            object only allows access via HTTPS.
     */
    protected void setHttpsOnly(boolean httpsOnly) {
        this.httpsOnly = httpsOnly;
    }

    /**
     * Returns a String that represents this instance.
     *
     * @param exportSecrets
     *            true to include sensitive data in the return string; otherwise, false.
     * @return A String that represents this object, optionally including sensitive data.
     */
    public abstract String toString(boolean exportSecrets);

    /**
     * Transforms a resource URI into a shared access signature URI, by appending a shared access token.
     *
     * @param resourceUri
     *            A java.net.URI object that represents the resource URI to be transformed.
     *
     * @return A java.net.URI object that represents the signature, including the resource URI and the
     *         shared access token.
     *
     * @throws StorageException
     *             If a storage service error occurred.
     * @throws URISyntaxException
     *             If the resource URI is not properly formatted.
     */
    public URI transformUri(final URI resourceUri) throws URISyntaxException, StorageException {
        return this.transformUri(resourceUri, null);
    }

    /**
     * Transforms a resource URI into a shared access signature URI, by appending a shared access token.
     *
     * @param resourceUri
     *            A StorageUri object that represents the resource URI to be transformed.
     *
     * @return A StorageUri object that represents the signature, including the resource URI and the
     *         shared access token.
     *
     * @throws StorageException
     *             If a storage service error occurred.
     * @throws URISyntaxException
     *             If the resource URI is not properly formatted.
     */
    public StorageUri transformUri(StorageUri resourceUri) throws URISyntaxException, StorageException {
        return this.transformUri(resourceUri, null /* opContext */);
    }

    /**
     * Transforms a resource URI into a shared access signature URI, by appending a shared access token and using the
     * specified operation context.
     *
     * @param resourceUri
     *            A java.net.URI object that represents the resource URI to be transformed.
     * @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.
     *
     * @return A java.net.URI object that represents the signature, including the resource URI and the
     *         shared access token.
     *
     * @throws StorageException
     *             If a storage service error occurred.
     * @throws URISyntaxException
     *             If the resource URI is not properly formatted.
     */
    public abstract URI transformUri(URI resourceUri, OperationContext opContext) throws URISyntaxException,
            StorageException;

    /**
     * Transforms a resource URI into a shared access signature URI, by appending a shared access token and using the
     * specified operation context.
     *
     * @param resourceUri
     *            A StorageUri object that represents the resource URI to be transformed.
     * @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.
     *
     * @return A StorageUri object that represents the signature, including the resource URI and the
     *         shared access token.
     *
     * @throws StorageException
     *             If a storage service error occurred.
     * @throws URISyntaxException
     *             If the resource URI is not properly formatted.
     */
    public abstract StorageUri transformUri(StorageUri resourceUri, OperationContext opContext)
            throws URISyntaxException, 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.analytics;

import java.net.URISyntaxException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumSet;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.TimeZone;

import com.microsoft.azure.storage.LoggingOperations;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.blob.BlobListingDetails;
import com.microsoft.azure.storage.blob.BlobRequestOptions;
import com.microsoft.azure.storage.blob.CloudBlob;
import com.microsoft.azure.storage.blob.CloudBlobDirectory;
import com.microsoft.azure.storage.blob.ListBlobItem;
import com.microsoft.azure.storage.core.SR;

/**
 * RESERVED FOR INTERNAL USE. Provides an overlay on the LazySegmentedIterator class for enumerating Storage Analytics
 * log blobs. This handles the logic for the listLogBlobs() methods found in the <code>CloudAnalyticsClient<code> class.
 */
class LogBlobIterator implements Iterator<ListBlobItem> {
    private static final String HOUR_STRING = "yyyy/MM/dd/HH";

    private static final String DAY_STRING = "yyyy/MM/dd";

    private static final String MONTH_STRING = "yyyy/MM";

    private static final String YEAR_STRING = "yyyy";

    private static final DateFormat HOUR_FORMAT = new SimpleDateFormat(HOUR_STRING);

    private static final DateFormat DAY_FORMAT = new SimpleDateFormat(DAY_STRING);

    private static final DateFormat MONTH_FORMAT = new SimpleDateFormat(MONTH_STRING);

    private static final DateFormat YEAR_FORMAT = new SimpleDateFormat(YEAR_STRING);

    private static final int HOUR_FORMAT_LENGTH = HOUR_STRING.length();

    private static final int DAY_FORMAT_LENGTH = DAY_STRING.length();

    private static final int MONTH_FORMAT_LENGTH = MONTH_STRING.length();

    /**
     * Holds a reference to the parent log CloudBlobDirectory.
     */
    private final CloudBlobDirectory logDirectory;

    /**
     * Holds the start date and time of the log range requested.
     */
    private Calendar startDate = null;

    /**
     * Holds the end date and time of the log range requested.
     */
    private Calendar endDate = null;

    /**
     * Holds the value of which log types are being requested.
     */
    private final EnumSet<LoggingOperations> operations;

    /**
     * Holds the value of which blob details are being requested.
     *
     * Note that presently we only allow none or metadata to be specified here.
     */
    private final EnumSet<BlobListingDetails> details;

    /**
     * Holds the request options to use when making the listBlob() requests.
     */
    private final BlobRequestOptions options;

    /**
     * Holds an object used to track the execution of the operation.
     */
    private final OperationContext opContext;

    /**
     * Holds the iterator for the current call.
     */
    private Iterator<ListBlobItem> currentIterator;

    /**
     * Represents the current time prefix being passed to listBlobs().
     */
    private String currentPrefixTime = null;

    /**
     * Represents the prefix that marks the end
     */
    private String endPrefix;

    /**
     * Represents whether a valid log has been already retrieved.
     */
    private Boolean isItemPending = false;

    /**
     * Represents whether this iterator has already passed the present moment or its corresponding endTime.
     */
    private Boolean isExpired = false;

    /**
     * The valid retrieved log that isItemPending refers to.
     */
    private ListBlobItem pendingItem;

    public LogBlobIterator(final CloudBlobDirectory logDirectory, final Date startDate, final Date endDate,
            final EnumSet<LoggingOperations> operations, final EnumSet<BlobListingDetails> details,
            final BlobRequestOptions options, final OperationContext opContext) {
        TimeZone gmtTime = TimeZone.getTimeZone("GMT");
        HOUR_FORMAT.setTimeZone(gmtTime);
        DAY_FORMAT.setTimeZone(gmtTime);
        MONTH_FORMAT.setTimeZone(gmtTime);
        YEAR_FORMAT.setTimeZone(gmtTime);

        this.logDirectory = logDirectory;
        this.operations = operations;
        this.details = details;
        this.opContext = opContext;

        if (options == null) {
            this.options = new BlobRequestOptions();
        }
        else {
            this.options = options;
        }

        if (startDate != null) {
            this.startDate = new GregorianCalendar();
            this.startDate.setTime(startDate);
            this.startDate.add(GregorianCalendar.MINUTE, (-this.startDate.get(GregorianCalendar.MINUTE)));
            this.startDate.setTimeZone(gmtTime);
        }
        if (endDate != null) {
            this.endDate = new GregorianCalendar();
            this.endDate.setTime(endDate);
            this.endDate.setTimeZone(gmtTime);
            this.endPrefix = this.logDirectory.getPrefix() + HOUR_FORMAT.format(this.endDate.getTime());
        }
    }

    @Override
    public boolean hasNext() {
        if (this.isItemPending) {
            // Short circuit
            return true;
        }

        try {
            if (this.currentIterator == null) {
                // If this is the first time, get the first iterator before entering loop.
                updateIterator();
            }
            while (!this.isExpired) {
                while (this.currentIterator.hasNext()) {
                    // Go through all of the logs in this iterator and see if any are of the correct type.
                    ListBlobItem current = this.currentIterator.next();
                    if (this.endDate == null || (current.getParent().getPrefix()).compareTo(this.endPrefix) <= 0) {
                        if (isCorrectLogType(current)) {
                            this.pendingItem = current;
                            this.isItemPending = true;
                            return true;
                        }
                    }
                    else {
                        // We have passed endTime
                        this.isExpired = true;
                        return false;
                    }
                }
                updateIterator();
            }
        }
        catch (final StorageException e) {
            final NoSuchElementException ex = new NoSuchElementException(SR.ENUMERATION_ERROR);
            ex.initCause(e);
            throw ex;
        }
        catch (final URISyntaxException e) {
            final NoSuchElementException ex = new NoSuchElementException(SR.ENUMERATION_ERROR);
            ex.initCause(e);
            throw ex;
        }

        return false;
    }

    @Override
    public ListBlobItem next() {
        if (this.isItemPending) {
            this.isItemPending = false;
            return this.pendingItem;
        }

        if (this.hasNext()) {
            return this.next();
        }
        else {
            throw new NoSuchElementException(SR.ITERATOR_EMPTY);
        }
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }

    /**
     * Validates that the log given is of the correct log type.
     *
     * @param current
     *            the current log
     * @return whether or not the log is of the correct type.
     */
    private boolean isCorrectLogType(ListBlobItem current) {
        HashMap<String, String> metadata = ((CloudBlob) current).getMetadata();
        String logType = metadata.get("LogType");

        if (logType == null) {
            return true;
        }

        if (this.operations.contains(LoggingOperations.READ) && logType.contains("read")) {
            return true;
        }

        if (this.operations.contains(LoggingOperations.WRITE) && logType.contains("write")) {
            return true;
        }

        if (this.operations.contains(LoggingOperations.DELETE) && logType.contains("delete")) {
            return true;
        }

        return false;
    }

    /**
     * Makes the next listBlob call if necessary and updates the currentIterator.
     *
     * @throws StorageException
     * @throws URISyntaxException
     */
    private void updateIterator() throws StorageException, URISyntaxException {
        if (this.currentPrefixTime != null && this.currentPrefixTime.isEmpty()) {
            // If we've already called listBlobs() with an empty prefix, don't do so again.
            this.isExpired = true;
            return;
        }

        GregorianCalendar now = new GregorianCalendar();
        now.add(GregorianCalendar.HOUR_OF_DAY, 1);
        now.setTimeZone(TimeZone.getTimeZone("GMT"));
        updatePrefix();
        if ((this.startDate == null || this.startDate.compareTo(now) <= 0)
                && (this.endDate == null || ((this.logDirectory.getPrefix() + this.currentPrefixTime)
                        .compareTo(this.endPrefix) <= 0))) {
            // Only make the next call if the prefix is still possible
            this.currentIterator = this.logDirectory.listBlobs(this.currentPrefixTime, true, this.details,
                    this.options, this.opContext).iterator();
        }
        else {
            // We are in the future.
            this.isExpired = true;
        }
    }

    /**
     * Updates the currentPrefixTime so that we can make a new call to listBlobs() with the next prefix.
     */
    private void updatePrefix() {
        if (this.startDate == null) {
            // startDate not specified
            this.currentPrefixTime = "";
        }
        else if (this.currentPrefixTime == null) {
            // First prefix
            this.currentPrefixTime = HOUR_FORMAT.format(this.startDate.getTime());
        }
        else if (this.currentPrefixTime.length() == HOUR_FORMAT_LENGTH) {
            // Increment the hour
            this.startDate.add(GregorianCalendar.HOUR_OF_DAY, 1);
            if (this.startDate.get(GregorianCalendar.HOUR_OF_DAY) != 0) {
                // If we are still within the same day, use the hour format
                this.currentPrefixTime = HOUR_FORMAT.format(this.startDate.getTime());
            }
            else {
                // If we've reached a day boundary, get the entire next day's logs
                this.currentPrefixTime = DAY_FORMAT.format(this.startDate.getTime());
            }
        }
        else if (this.currentPrefixTime.length() == DAY_FORMAT_LENGTH) {
            // Increment the day
            this.startDate.add(GregorianCalendar.DAY_OF_MONTH, 1);
            if (this.startDate.get(GregorianCalendar.DAY_OF_MONTH) != 1) {
                // If we are still within the same month, use the day format
                this.currentPrefixTime = DAY_FORMAT.format(this.startDate.getTime());
            }
            else {
                // If we've reached a month boundary, get the entire next month's logs
                this.currentPrefixTime = MONTH_FORMAT.format(this.startDate.getTime());
            }
        }
        else if (this.currentPrefixTime.length() == MONTH_FORMAT_LENGTH) {
            // Increment the month
            this.startDate.add(GregorianCalendar.MONTH, 1);
            if (this.startDate.get(GregorianCalendar.MONTH) != 13) { // Undecember
                // If we are still within the same year, use the month format
                this.currentPrefixTime = MONTH_FORMAT.format(this.startDate.getTime());
            }
            else {
                // If we've reached a year boundary, get the entire next year's logs
                this.currentPrefixTime = YEAR_FORMAT.format(this.startDate.getTime());
            }
        }
        else {
            // Continue to increment year and get the next year's worth of logs.
            this.startDate.add(GregorianCalendar.YEAR, 1);
            this.currentPrefixTime = YEAR_FORMAT.format(this.startDate.getTime());
        }
    }
}