/**
 * 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.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;

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.StorageExtendedErrorInformation;
import com.microsoft.azure.storage.core.JsonUtilities;
import com.microsoft.azure.storage.core.StorageRequest;

/***
 * RESERVED FOR INTERNAL USE. A class to help parse the error details from an input stream, specific to tables
 */
final class TableStorageErrorDeserializer {

    /**
     * Gets the Extended Error information.
     *
     * @return the Extended Error information.
     *
     * @param reader
     *            the input stream to read error details from.
     * @param format
     *            The {@link TablePayloadFormat} to use for parsing
     * @throws IOException
     *             if an error occurs while accessing the stream with Json.
     */
    public static StorageExtendedErrorInformation getExtendedErrorInformation(final Reader reader,
            final TablePayloadFormat format) throws JsonParseException, IOException {

        JsonFactory jsonFactory = new JsonFactory();
        JsonParser parser = jsonFactory.createParser(reader);
        try {
            final StorageExtendedErrorInformation errorInfo = new StorageExtendedErrorInformation();

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

            JsonUtilities.assertIsStartObjectJsonToken(parser);

            parser.nextToken();
            JsonUtilities.assertIsFieldNameJsonToken(parser);
            JsonUtilities.assertIsExpectedFieldName(parser, "odata.error");

            // start getting extended error information
            parser.nextToken();
            JsonUtilities.assertIsStartObjectJsonToken(parser);

            // get code
            parser.nextValue();
            JsonUtilities.assertIsExpectedFieldName(parser, TableConstants.ErrorConstants.ERROR_CODE);
            errorInfo.setErrorCode(parser.getValueAsString());

            // get message
            parser.nextToken();
            JsonUtilities.assertIsFieldNameJsonToken(parser);
            JsonUtilities.assertIsExpectedFieldName(parser, TableConstants.ErrorConstants.ERROR_MESSAGE);

            parser.nextToken();
            JsonUtilities.assertIsStartObjectJsonToken(parser);

            parser.nextValue();
            JsonUtilities.assertIsExpectedFieldName(parser, "lang");

            parser.nextValue();
            JsonUtilities.assertIsExpectedFieldName(parser, "value");
            errorInfo.setErrorMessage(parser.getValueAsString());

            parser.nextToken();
            JsonUtilities.assertIsEndObjectJsonToken(parser);

            parser.nextToken();

            // get innererror if it exists
            if (parser.getCurrentToken() == JsonToken.FIELD_NAME) {
                JsonUtilities.assertIsExpectedFieldName(parser, TableConstants.ErrorConstants.INNER_ERROR);
                errorInfo.getAdditionalDetails().putAll(parseJsonErrorException(parser));
                parser.nextToken();
            }

            // end code object
            JsonUtilities.assertIsEndObjectJsonToken(parser);

            // end odata.error object
            parser.nextToken();
            JsonUtilities.assertIsEndObjectJsonToken(parser);

            return errorInfo;
        }
        finally {
            parser.close();
        }
    }

    /**
     * Parse the table extended error information from the response body.
     *
     * @param request
     *            the request whose body to examine for the error information.
     * @return The {@link StorageExtendedErrorInformation} parsed from the body or null if parsing fails or the request
     *         contains no body.
     */
    public static StorageExtendedErrorInformation parseErrorDetails(StorageRequest<CloudTableClient, ?, ?> request) {
        try {
            if (request == null || request.getConnection().getErrorStream() == null) {
                return null;
            }

            return getExtendedErrorInformation(new InputStreamReader(
            		request.getConnection().getErrorStream()),
            		TablePayloadFormat.Json);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Parses the error exception details from the Json-formatted response.
     *
     * @param parser
     *            the {@link JsonParser} to use for parsing
     * @throws IOException
     *             if an error occurs while accessing the stream with Json.
     * @throws JsonParseException
     *             if an error occurs while parsing the stream.
     */
    private static HashMap<String, String[]> parseJsonErrorException(JsonParser parser) throws JsonParseException,
            IOException {
        HashMap<String, String[]> additionalDetails = new HashMap<String, String[]>();

        parser.nextToken();
        JsonUtilities.assertIsStartObjectJsonToken(parser);

        parser.nextToken();
        JsonUtilities.assertIsFieldNameJsonToken(parser);

        while (parser.getCurrentToken() != JsonToken.END_OBJECT) {
            if (parser.getCurrentName().equals(TableConstants.ErrorConstants.ERROR_MESSAGE)) {
                parser.nextToken();
                additionalDetails.put(TableConstants.ErrorConstants.ERROR_MESSAGE,
                        new String[] { parser.getValueAsString() });
            }
            else if (parser.getCurrentName().equals(TableConstants.ErrorConstants.ERROR_EXCEPTION_TYPE)) {
                parser.nextToken();
                additionalDetails.put(TableConstants.ErrorConstants.ERROR_EXCEPTION_TYPE,
                        new String[] { parser.getValueAsString() });
            }
            else if (parser.getCurrentName().equals(TableConstants.ErrorConstants.ERROR_EXCEPTION_STACK_TRACE)) {
                parser.nextToken();
                additionalDetails
                        .put(Constants.ERROR_EXCEPTION_STACK_TRACE, new String[] { parser.getValueAsString() });
            }
            parser.nextToken();
        }

        return additionalDetails;
    }
}


/**
 * 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.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

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

/**
 * RESERVED FOR INTERNAL USE. A class used to deserialize SharedAccessPolicies.
 */
public class SharedAccessPolicyHandler<T extends SharedAccessPolicy> extends DefaultHandler {

    private final Stack<String> elementStack = new Stack<String>();
    private StringBuilder bld = new StringBuilder();

    private final HashMap<String, T> policies = new HashMap<String, T>();
    private final Class<T> policyClassType;

    private String id;
    private T policy;

    private SharedAccessPolicyHandler(final Class<T> cls) {
        this.policyClassType = cls;
    }

    /**
     * RESERVED FOR INTERNAL USE. Gets the HashMap of SharedAccessPolicies from the response.
     *
     * @param stream
     *            the stream to read from
     * @param cls
     *            the SharedAccessPolicy class type
     * @return the HashMap of SharedAccessPolicies from the response
     * @throws SAXException
     * @throws ParserConfigurationException
     * @throws ParseException
     *             if a date is incorrectly encoded in the stream
     * @throws IOException
     */
    public static <T extends SharedAccessPolicy> HashMap<String, T> getAccessIdentifiers(final InputStream stream,
            final Class<T> cls) throws ParserConfigurationException, SAXException, IOException {
        SAXParser saxParser = Utility.getSAXParser();
        SharedAccessPolicyHandler<T> handler = new SharedAccessPolicyHandler<T>(cls);
        saxParser.parse(stream, handler);

        return handler.policies;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        this.elementStack.push(localName);

        if (Constants.SIGNED_IDENTIFIER_ELEMENT.equals(localName)) {
            this.id = null;

            try {
                this.policy = this.policyClassType.newInstance();
            }
            catch (Exception e) {
                throw new SAXException(e);
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        String currentNode = this.elementStack.pop();

        // if the node popped from the stack and the localName don't match, the xml document is improperly formatted
        if (!localName.equals(currentNode)) {
            throw new SAXException(SR.INVALID_RESPONSE_RECEIVED);
        }

        String value = this.bld.toString();
        if (value.isEmpty()) {
            value = null;
        }

        if (Constants.SIGNED_IDENTIFIER_ELEMENT.equals(currentNode)) {
            this.policies.put(this.id, this.policy);
        }
        else if (Constants.ID.equals(currentNode)) {
            this.id = value;
        }
        else if (Constants.START.equals(currentNode)) {
            try {
                this.policy.setSharedAccessStartTime(Utility.parseDate(value));
            }
            catch (IllegalArgumentException e) {
                throw new SAXException(e);
            }
        }
        else if (Constants.EXPIRY.equals(currentNode)) {
            try {
                this.policy.setSharedAccessExpiryTime(Utility.parseDate(value));
            }
            catch (IllegalArgumentException e) {
                throw new SAXException(e);
            }
        }
        else if (Constants.PERMISSION.equals(currentNode)) {
            this.policy.setPermissionsFromString(value);
        }

        this.bld = new StringBuilder();
    }

    @Override
    public void characters(char ch[], int start, int length) throws SAXException {
        this.bld.append(ch, start, length);
    }
}


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

import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;

import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.StorageUri;
import com.microsoft.azure.storage.core.BaseResponse;
import com.microsoft.azure.storage.core.PathUtility;
import com.microsoft.azure.storage.core.Utility;

/**
 * RESERVED FOR INTERNAL USE. A class used to parse the response from file, directory, and share operations.
 */
final class FileResponse extends BaseResponse {
    /**
     * Gets the copyState
     *
     * @param request
     *            The response from server.
     * @return The CopyState.
     * @throws URISyntaxException
     * @throws ParseException
     */
    public static CopyState getCopyState(final HttpURLConnection request) throws URISyntaxException, ParseException {
        String copyStatusString = request.getHeaderField(Constants.HeaderConstants.COPY_STATUS);
        if (!Utility.isNullOrEmpty(copyStatusString)) {
            final CopyState copyState = new CopyState();

            copyState.setStatus(CopyStatus.parse(copyStatusString));
            copyState.setCopyId(request.getHeaderField(Constants.HeaderConstants.COPY_ID));
            copyState.setStatusDescription(request.getHeaderField(Constants.HeaderConstants.COPY_STATUS_DESCRIPTION));

            final String copyProgressString = request.getHeaderField(Constants.HeaderConstants.COPY_PROGRESS);
            if (!Utility.isNullOrEmpty(copyProgressString)) {
                String[] progressSequence = copyProgressString.split("/");
                copyState.setBytesCopied(Long.parseLong(progressSequence[0]));
                copyState.setTotalBytes(Long.parseLong(progressSequence[1]));
            }

            final String copySourceString = request.getHeaderField(Constants.HeaderConstants.COPY_SOURCE);
            if (!Utility.isNullOrEmpty(copySourceString)) {
                copyState.setSource(new URI(copySourceString));
            }

            final String copyCompletionTimeString =
                    request.getHeaderField(Constants.HeaderConstants.COPY_COMPLETION_TIME);
            if (!Utility.isNullOrEmpty(copyCompletionTimeString)) {
                copyState.setCompletionTime(Utility.parseRFC1123DateFromStringInGMT(copyCompletionTimeString));
            }

            return copyState;
        }
        else {
            return null;
        }
    }

    /**
     * Gets the FileShareAttributes from the given request.
     *
     * @param request
     *            the request to get attributes from
     * @param usePathStyleUris
     *            a value indicating if the account is using pathSytleUris
     * @return the FileShareAttributes from the given request.
     * @throws StorageException
     */
    public static FileShareAttributes getFileShareAttributes(final HttpURLConnection request,
            final boolean usePathStyleUris) throws StorageException {
        final FileShareAttributes shareAttributes = new FileShareAttributes();
        final FileShareProperties shareProperties = shareAttributes.getProperties();
        shareProperties.setEtag(BaseResponse.getEtag(request));
        shareProperties.setShareQuota(parseShareQuota(request));
        shareProperties.setLastModified(new Date(request.getLastModified()));
        shareAttributes.setMetadata(getMetadata(request));

        return shareAttributes;
    }

    /**
     * Gets the FileDirectoryAttributes from the given request.
     *
     * @param request
     *            the request to get attributes from.
     * @param usePathStyleUris
     *            a value indicating if the account is using pathSytleUris.
     * @return the FileDirectoryAttributes from the given request.
     * @throws StorageException
     */
    public static FileDirectoryAttributes getFileDirectoryAttributes(final HttpURLConnection request,
            final boolean usePathStyleUris) throws StorageException {
        final FileDirectoryAttributes directoryAttributes = new FileDirectoryAttributes();
        URI tempURI;
        try {
            tempURI = PathUtility.stripSingleURIQueryAndFragment(request.getURL().toURI());
        }
        catch (final URISyntaxException e) {
            final StorageException wrappedUnexpectedException = Utility.generateNewUnexpectedStorageException(e);
            throw wrappedUnexpectedException;
        }

        directoryAttributes.setName(PathUtility.getDirectoryNameFromURI(tempURI, usePathStyleUris));

        final FileDirectoryProperties directoryProperties = directoryAttributes.getProperties();
        directoryProperties.setEtag(BaseResponse.getEtag(request));
        directoryProperties.setLastModified(new Date(request.getLastModified()));
        directoryAttributes.setMetadata(getMetadata(request));
        directoryProperties.setServerEncrypted(
                Constants.TRUE.equals(request.getHeaderField(Constants.HeaderConstants.SERVER_ENCRYPTED)));

        return directoryAttributes;
    }

    /**
     * Gets the CloudFileAttributes from the given request
     *
     * @param request
     *            The response from server.
     * @param resourceURI
     *            The file uri to set.
     * @return the CloudFileAttributes from the given request
     * @throws ParseException
     * @throws URISyntaxException
     */
    public static FileAttributes getFileAttributes(final HttpURLConnection request, final StorageUri resourceURI)
            throws URISyntaxException, ParseException {
        final FileAttributes fileAttributes = new FileAttributes();
        final FileProperties properties = fileAttributes.getProperties();

        properties.setCacheControl(request.getHeaderField(Constants.HeaderConstants.CACHE_CONTROL));
        properties.setContentDisposition(request.getHeaderField(Constants.HeaderConstants.CONTENT_DISPOSITION));
        properties.setContentEncoding(request.getHeaderField(Constants.HeaderConstants.CONTENT_ENCODING));
        properties.setContentLanguage(request.getHeaderField(Constants.HeaderConstants.CONTENT_LANGUAGE));

        // For range gets, only look at 'x-ms-content-md5' for overall MD5
        if (!Utility.isNullOrEmpty(request.getHeaderField(Constants.HeaderConstants.CONTENT_RANGE))) {
            properties.setContentMD5(request.getHeaderField(FileConstants.FILE_CONTENT_MD5_HEADER));
        }
        else {
            properties.setContentMD5(request.getHeaderField(Constants.HeaderConstants.CONTENT_MD5));
        }

        properties.setContentType(request.getHeaderField(Constants.HeaderConstants.CONTENT_TYPE));
        properties.setEtag(BaseResponse.getEtag(request));
        properties.setCopyState(FileResponse.getCopyState(request));
        properties.setServerEncrypted(
                Constants.TRUE.equals(request.getHeaderField(Constants.HeaderConstants.SERVER_ENCRYPTED)));

        final Calendar lastModifiedCalendar = Calendar.getInstance(Utility.LOCALE_US);
        lastModifiedCalendar.setTimeZone(Utility.UTC_ZONE);
        lastModifiedCalendar.setTime(new Date(request.getLastModified()));
        properties.setLastModified(lastModifiedCalendar.getTime());

        final String rangeHeader = request.getHeaderField(Constants.HeaderConstants.CONTENT_RANGE);
        final String xContentLengthHeader = request.getHeaderField(FileConstants.CONTENT_LENGTH_HEADER);

        if (!Utility.isNullOrEmpty(rangeHeader)) {
            properties.setLength(Long.parseLong(rangeHeader.split("/")[1]));
        }
        else if (!Utility.isNullOrEmpty(xContentLengthHeader)) {
            properties.setLength(Long.parseLong(xContentLengthHeader));
        }
        else {
            // using this instead of the request property since the request
            // property only returns an int.
            final String contentLength = request.getHeaderField(Constants.HeaderConstants.CONTENT_LENGTH);

            if (!Utility.isNullOrEmpty(contentLength)) {
                properties.setLength(Long.parseLong(contentLength));
            }
        }

        fileAttributes.setStorageUri(resourceURI);

        fileAttributes.setMetadata(BaseResponse.getMetadata(request));

        return fileAttributes;
    }

    /**
     * Parses out the share quota value from a java.net.HttpURLConnection.
     *
     * @param request
     *            the request to get attributes from
     * @return the share quota (in GB) or null if none is specified
     */
    static Integer parseShareQuota(final HttpURLConnection request) {
        Integer shareQuota = request.getHeaderFieldInt(FileConstants.SHARE_QUOTA_HEADER, -1);
        return (shareQuota == -1) ? null : shareQuota;
    }

    /**
     * Gets the snapshot ID from the request header.
     *
     * @param request
     *            The response from server.
     * @return the snapshot ID from the request header.
     */
    public static String getSnapshotTime(final HttpURLConnection request) {
        return request.getHeaderField(Constants.HeaderConstants.SNAPSHOT_ID_HEADER);
    }

    /**
     * Private Default Ctor
     */
    private FileResponse() {
        super();
    }
}


/**
 * 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.net.URI;
import java.util.HashMap;
import java.util.Map;

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

/**
 * Reserved for internal use. A class that represents a given MIME Part.
 */
final class MimePart {
    int httpStatusCode = -1;
    String httpStatusMessage;
    HashMap<String, String> headers = new HashMap<String, String>();
    String payload;
    TableOperationType op;
    URI requestIdentity;

    String toRequestString() {
        StringBuilder builder = new StringBuilder();

        // append mime part header
        appendHeader(builder);

        builder.append(String.format("%s %s HTTP/1.1\r\n", getHttpVerbForOperation(this.op),
                this.requestIdentity.toString()));

        for (Map.Entry<String, String> header : this.headers.entrySet()) {
            builder.append(String.format("%s: %s\r\n", header.getKey(), header.getValue()));
        }

        builder.append("\r\n");

        if (this.payload != null) {
            builder.append(this.payload);
        }

        return builder.toString();
    }

    static void appendHeader(StringBuilder builder) {
        builder.append("Content-Type: application/http\r\n");
        builder.append("Content-Transfer-Encoding: binary\r\n\r\n");
    }

    /**
     * Reserved for internal use. Returns the HTTP verb for a table operation.
     *
     * @param operationType
     *            The {@link TableOperation} instance to get the HTTP verb for.
     * @return
     *         A String containing the HTTP verb to use with the operation.
     */
    static String getHttpVerbForOperation(final TableOperationType operationType) {
        if (operationType == TableOperationType.INSERT) {
            return "POST";
        }
        else if (operationType == TableOperationType.DELETE) {
            return "DELETE";
        }
        else if (operationType == TableOperationType.MERGE || operationType == TableOperationType.INSERT_OR_MERGE) {
            return "MERGE";
        }
        else if (operationType == TableOperationType.REPLACE || operationType == TableOperationType.INSERT_OR_REPLACE) {
            return "PUT";
        }
        else if (operationType == TableOperationType.RETRIEVE) {
            return "GET";
        }
        else {
            throw new IllegalArgumentException(SR.UNKNOWN_TABLE_OPERATION);
        }
    }
}


/**
 * 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.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

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

/**
 * An exception that results when a table storage service operation fails to complete successfully.
 */
public final class TableServiceException extends StorageException {

    private static final long serialVersionUID = 6037366449663934891L;

    private TableOperation operation;

    /**
     * Reserved for internal use. A static factory method to create a {@link TableServiceException} instance using
     * the specified parameters.
     * @param res
     *            A {@link RequestResult} containing the result of the table storage service operation.
     * @param op
     *            The {@link TableOperation} representing the table operation that caused the exception.
     * @param inStream
     *            The java.io.InputStream of the error response from the table operation request.
     * @param format
     *            The {@link TablePayloadFormat} to use for parsing
     *
     * @return
     *         A {@link TableServiceException} instance initialized with values from the input parameters.
     */
    protected static TableServiceException generateTableServiceException(RequestResult res, TableOperation op,
            InputStream inStream, TablePayloadFormat format) {
        return new TableServiceException(res.getStatusCode(), res.getStatusMessage(), op,
                new InputStreamReader(inStream), format);
    }

    /**
     * Reserved for internal use. Constructs a TableServiceException instance using the specified HTTP
     * status code, message, operation, and stream reader.
     *
     * @param httpStatusCode
     *            The int HTTP Status Code value returned by the table operation that caused the exception.
     * @param message
     *            A String description of the error that caused the exception.
     * @param operation
     *            The {@link TableOperation} object representing the table operation that was in progress when the
     *            exception occurred.
     * @param reader
     *            The Java.IO.Stream derived stream reader for the HTTP request results returned by the
     *            table operation, if any.
     * @param format
     *            The {@link TablePayloadFormat} to use for parsing
     */
    protected TableServiceException(final int httpStatusCode, final String message, final TableOperation operation,
            final Reader reader, final TablePayloadFormat format) {
        super(null, message, httpStatusCode, null, null);
        this.operation = operation;

        if (reader != null) {
            try {
                this.extendedErrorInformation = TableStorageErrorDeserializer.getExtendedErrorInformation(reader,
                        format);
                this.errorCode = this.extendedErrorInformation.getErrorCode();
            }
            catch (Exception e) {
                // no-op, if error parsing fails, just throw original exception.
            }
        }
    }

    /**
     * Gets the table operation that caused the TableServiceException to be thrown.
     *
     * @return
     *         The {@link TableOperation} object representing the table operation that caused this
     *         {@link TableServiceException} to be thrown.
     */
    public TableOperation getOperation() {
        return this.operation;
    }

    /**
     * Reserved for internal use. Sets the table operation that caused the TableServiceException to be
     * thrown.
     *
     * @param operation
     *            The {@link TableOperation} object representing the table operation that caused this
     *            {@link TableServiceException} to be thrown.
     */
    protected void setOperation(final TableOperation operation) {
        this.operation = operation;
    }
}


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

import java.io.FileInputStream;

import com.microsoft.azure.storage.LocationMode;
import com.microsoft.azure.storage.RequestOptions;
import com.microsoft.azure.storage.ServiceClient;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.Utility;

/**
 * Represents a set of options that may be specified on a request.
 */
public final class FileRequestOptions extends RequestOptions {

    /**
     * Represents the concurrent number of simultaneous requests per operation. If it's null, it will be set to the
     * value specified by the cloud file client's {@link CloudFileClient#getConcurrentRequestCount} method during upload
     * operations.
     */
    private Integer concurrentRequestCount = null;

    /**
     * Specifies whether a range PUT or GET operation will use the Content-MD5 header to enforce transactional security.
     * All partial file uploads or downloads will be restricted to 4 MB. The default value is false.
     */
    private Boolean useTransactionalContentMD5 = null;

    /**
     * Specifies whether the file's ContentMD5 header should be set on uploads.
     * The default value is false.
     */
    private Boolean storeFileContentMD5 = null;

    /**
     * Specifies whether download and {@link FileInputStream} methods should ignore the file's ContentMD5 header. The
     * default value is false.
     */
    private Boolean disableContentMD5Validation = null;

    /**
     * Creates an instance of the FileRequestOptions class.
     */
    public FileRequestOptions() {
        // Empty Default Constructor.
    }

    /**
     * Creates an instance of the FileRequestOptions class by copying values from another
     * FileRequestOptions instance.
     *
     * @param other
     *            A {@link FileRequestOptions} object which represents the file request options to copy.
     */
    public FileRequestOptions(final FileRequestOptions other) {
        super(other);
        if (other != null) {
            this.setConcurrentRequestCount(other.getConcurrentRequestCount());
            this.setDisableContentMD5Validation(other.getDisableContentMD5Validation());
            this.setStoreFileContentMD5(other.getStoreFileContentMD5());
            this.setUseTransactionalContentMD5(other.getUseTransactionalContentMD5());
        }
    }

    /**
     * Uses the concurrent request count from the specified client if null, sets a default value for
     * everything else, and sets defaults as defined in the parent class.
     *
     * @param options
     *            The input options to copy from when applying defaults
     * @param client
     *            A {@link CloudFileClient} object that represents the service client used to set the default timeout
     *            interval and retry policy, if they are null. Additionally, if the
     *            {@link #concurrentRequestCount} field's value is null, it will be set to the value specified by the
     *            cloud file client's {@link CloudFileClient#getConcurrentRequestCount} method.
     */
    protected static final FileRequestOptions populateAndApplyDefaults(final FileRequestOptions options,
            final CloudFileClient client) {
        return FileRequestOptions.populateAndApplyDefaults(options, client, true);
    }

    /**
     * Uses the concurrent request count from the specified client if null, sets a default value for
     * everything else, and sets defaults as defined in the parent class.
     *
     * @param options
     *            The input options to copy from when applying defaults
     * @param client
     *            A {@link CloudFileClient} object that represents the service client used to set the default timeout
     *            interval and retry policy, if they are null. Additionally, if the
     *            {@link #concurrentRequestCount} field's value is null, it will be set to the value specified by the
     *            cloud blob client's {@link CloudFileClient#getConcurrentRequestCount} method.
     * @param setStartTime
     *            whether to initialize the startTimeInMs field, or not
     */
    protected static final FileRequestOptions populateAndApplyDefaults(final FileRequestOptions options,
            final CloudFileClient client, final boolean setStartTime) {
        FileRequestOptions modifiedOptions = new FileRequestOptions(options);
        FileRequestOptions.populateRequestOptions(modifiedOptions, client.getDefaultRequestOptions(), setStartTime);
        FileRequestOptions.applyDefaults(modifiedOptions);
        return modifiedOptions;
    }

    /**
     * Applies defaults to the options passed in.
     *
     * @param modifiedOptions
     *          The options to apply defaults to.
     */
    protected static void applyDefaults(final FileRequestOptions modifiedOptions) {
        Utility.assertNotNull("modifiedOptions", modifiedOptions);
        RequestOptions.applyBaseDefaultsInternal(modifiedOptions);
        if (modifiedOptions.getConcurrentRequestCount() == null) {
            modifiedOptions.setConcurrentRequestCount(FileConstants.DEFAULT_CONCURRENT_REQUEST_COUNT);
        }

        if (modifiedOptions.getUseTransactionalContentMD5() == null) {
            modifiedOptions.setUseTransactionalContentMD5(false);
        }

        if (modifiedOptions.getStoreFileContentMD5() == null) {
            modifiedOptions.setStoreFileContentMD5(false);
        }

        if (modifiedOptions.getDisableContentMD5Validation() == null) {
            modifiedOptions.setDisableContentMD5Validation(false);
        }
    }

    /**
     * Populates any null fields in the first requestOptions object with values from the second requestOptions object.
     */
    private static void populateRequestOptions(FileRequestOptions modifiedOptions,
            final FileRequestOptions clientOptions, boolean setStartTime) {
        RequestOptions.populateRequestOptions(modifiedOptions, clientOptions, setStartTime);
        if (modifiedOptions.getConcurrentRequestCount() == null) {
            modifiedOptions.setConcurrentRequestCount(clientOptions.getConcurrentRequestCount());
        }
    }

    /**
     * Gets the concurrent number of simultaneous requests per operation. For more information about concurrent request
     * count defaults, see {@link #setConcurrentRequestCount(Integer)}.
     *
     * @return the concurrentRequestCount
     */
    public Integer getConcurrentRequestCount() {
        return this.concurrentRequestCount;
    }

    /**
     * Gets whether a range PUT or GET operation will use the Content-MD5 header to enforce transactional security.
     * All partial file uploads or downloads will be restricted to 4 MB. For more information about transactional
     * content MD5 defaults, see {@link #setUseTransactionalContentMD5(Boolean)}.
     *
     * @return the useTransactionalContentMD5
     */
    public Boolean getUseTransactionalContentMD5() {
        return this.useTransactionalContentMD5;
    }

    /**
     * Gets whether the file's ContentMD5 header should be set on uploads. For more information about storing file
     * content MD5 defaults, see {@link #setStoreFileContentMD5(Boolean)} .
     *
     * @return the storeFileContentMD5
     */
    public Boolean getStoreFileContentMD5() {
        return this.storeFileContentMD5;
    }

    /**
     * Gets whether download and {@link FileInputStream} methods should ignore the file's ContentMD5 header. For more
     * information about disabling content MD5 validation defaults, see {@link #setDisableContentMD5Validation(Boolean)}
     * .
     *
     * @return the disableContentMD5Validation
     */
    public Boolean getDisableContentMD5Validation() {
        return this.disableContentMD5Validation;
    }

    /**
     * Sets the concurrent number of simultaneous requests per operation.
     *

     * The default concurrent request count is set in the client and is by default 1, indicating no concurrency. You can
     * change the concurrent request count on this request by setting this property. You can also change the value on
     * the {@link CloudFileClient#getDefaultRequestOptions()} object so that all subsequent requests made via the
     * service client will use that concurrent request count.
     *
     * @param concurrentRequestCount
     *            the concurrentRequestCount to set
     */
    public void setConcurrentRequestCount(final Integer concurrentRequestCount) {
        this.concurrentRequestCount = concurrentRequestCount;
    }

    /**
     * Sets whether a range PUT or GET operation will use the Content-MD5 header to enforce transactional security.
     *

     * The default useTransactionalContentMD5 value is set in the client and is by default false. You can
     * change the useTransactionalContentMD5 value on this request by setting this property. You can also change the
     * value on the {@link CloudFileClient#getDefaultRequestOptions()} object so that all subsequent requests made via
     * the service client will use that useTransactionalContentMD5 value.
     *
     * @param useTransactionalContentMD5
     *            the useTransactionalContentMD5 to set
     */
    public void setUseTransactionalContentMD5(final Boolean useTransactionalContentMD5) {
        this.useTransactionalContentMD5 = useTransactionalContentMD5;
    }

    /**
     * Sets whether the file's ContentMD5 header should be set on uploads.
     *

     * The default storeFileContentMD5 value is set in the client and is by default true. You can change
     * the storeFileContentMD5 value on this request by setting this property. You can also change the value on the
     * {@link CloudFileClient#getDefaultRequestOptions()} object so that all subsequent requests made via the service
     * client will use that storeFileContentMD5 value.
     *
     * @param storeFileContentMD5
     *            the storeFileContentMD5 to set
     */
    public void setStoreFileContentMD5(final Boolean storeFileContentMD5) {
        this.storeFileContentMD5 = storeFileContentMD5;
    }

    /**
     * Sets whether download and {@link FileInputStream} methods should ignore the file's ContentMD5 header.
     *

     * The default disableContentMD5Validation value is set in the client and is by default false. You can
     * change the disableContentMD5Validation value on this request by setting this property. You can also change the
     * value on the {@link CloudFileClient#getDefaultRequestOptions()} object so that all subsequent requests made via
     * the service client will use that disableContentMD5Validation value.
     *
     * @param disableContentMD5Validation
     *            the disableContentMD5Validation to set
     */
    public void setDisableContentMD5Validation(final Boolean disableContentMD5Validation) {
        this.disableContentMD5Validation = disableContentMD5Validation;
    }

    /**
     * Sets the {@link LocationMode} for this request.
     *

     * The default {@link LocationMode} is set in the client and is by default {@link LocationMode#PRIMARY_ONLY}. You
     * can change the {@link LocationMode} on this request by setting this property. You can also change the value on
     * the {@link ServiceClient#getDefaultRequestOptions()} object so that all subsequent requests made via the service
     * client will use that {@link LocationMode}.
     *
     * @param locationMode
     *            the locationMode to set
     */
    @Override
    public final void setLocationMode(final LocationMode locationMode) {
        if (locationMode != null && !locationMode.equals(LocationMode.PRIMARY_ONLY)) {
            throw new UnsupportedOperationException(SR.PRIMARY_ONLY_COMMAND);
        }

        super.setLocationMode(locationMode);
    }
}


/**
 * 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.text.ParseException;
import java.util.Date;
import java.util.UUID;

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

/**
 * A class which represents a single typed property value in a table entity. An {@link EntityProperty} stores the data
 * type as an {@link EdmType}. The value, which may be null for object types, but not for primitive types,
 * is serialized and stored as a String.
 *

 * {@link EntityProperty} provides overloaded constructors and overloads of the setValue method for
 * supported value types. Each overloaded constructor or setValue method sets the {@link EdmType} and
 * serializes the value appropriately based on the parameter type.
 *

 * Use one of the getValueAsType methods to deserialize an {@link EntityProperty} as the
 * appropriate type. The method will throw a {@link ParseException} or {@link IllegalArgumentException} if the
 * {@link EntityProperty} cannot be deserialized as that type.
 */
public final class EntityProperty {
    private String value;
    private Class<?> type;
    private EdmType edmType = EdmType.NULL;

    /**
     * Flag that specifies whether the client should look to correct Date values stored on a {@link TableEntity}
     * that may have been written using versions of this library prior to 0.4.0.
     * See here for more details.
     */
    private boolean dateBackwardCompatibility = false;

    /**
     * Reserved for internal use. Constructs an {@link EntityProperty} instance from a Object value type,
     * and verifies that the value can be interpreted as the specified data type.
     *
     * @param value
     *            The Object to convert to a string and store.
     */
    protected EntityProperty(final String value, final Class<?> type) {
        this.type = type;
        this.value = value;
        if (type.equals(byte[].class)) {
            this.getValueAsByteArray();
            this.edmType = EdmType.BINARY;
        }
        else if (type.equals(Byte[].class)) {
            this.getValueAsByteObjectArray();
            this.edmType = EdmType.BINARY;
        }
        else if (type.equals(String.class)) {
            this.edmType = EdmType.STRING;
        }
        else if (type.equals(boolean.class)) {
            this.getValueAsBoolean();
            this.edmType = EdmType.BOOLEAN;
        }
        else if (type.equals(Boolean.class)) {
            this.getValueAsBooleanObject();
            this.edmType = EdmType.BOOLEAN;
        }
        else if (type.equals(Date.class)) {
            this.getValueAsDate();
            this.edmType = EdmType.DATE_TIME;
        }
        else if (type.equals(double.class)) {
            this.getValueAsDouble();
            this.edmType = EdmType.DOUBLE;
        }
        else if (type.equals(Double.class)) {
            this.getValueAsDoubleObject();
            this.edmType = EdmType.DOUBLE;
        }
        else if (type.equals(UUID.class)) {
            this.getValueAsUUID();
            this.edmType = EdmType.GUID;
        }
        else if (type.equals(int.class)) {
            this.getValueAsInteger();
            this.edmType = EdmType.INT32;
        }
        else if (type.equals(Integer.class)) {
            this.getValueAsIntegerObject();
            this.edmType = EdmType.INT32;
        }
        else if (type.equals(long.class)) {
            this.getValueAsLong();
            this.edmType = EdmType.INT64;
        }
        else if (type.equals(Long.class)) {
            this.getValueAsLongObject();
            this.edmType = EdmType.INT64;
        }
        else {
            throw new IllegalArgumentException(String.format(SR.TYPE_NOT_SUPPORTED, type.toString()));
        }
    }

    /**
     * Reserved for internal use. Constructs an {@link EntityProperty} instance from a Object value and a
     * data type, and verifies that the value can be interpreted as the specified data type.
     *
     * @param value
     *            The Object to convert to a string and store.
     * @param edmType
     *            The Class<?> type of the value to construct.
     */
    protected EntityProperty(final Object value, final Class<?> type) {
        if (type.equals(byte[].class)) {
            setValue((byte[]) value);
            this.type = type;
        }
        else if (type.equals(Byte[].class)) {
            setValue((Byte[]) value);
            this.type = type;
        }
        else if (type.equals(String.class)) {
            setValue((String) value);
            this.type = type;
        }
        else if (type.equals(boolean.class)) {
            setValue(((Boolean) value).booleanValue());
            this.type = type;
        }
        else if (type.equals(Boolean.class)) {
            setValue((Boolean) value);
            this.type = type;
        }
        else if (type.equals(double.class)) {
            setValue(((Double) value).doubleValue());
            this.type = type;
        }
        else if (type.equals(Double.class)) {
            setValue((Double) value);
            this.type = type;
        }
        else if (type.equals(UUID.class)) {
            setValue((UUID) value);
            this.type = type;
        }
        else if (type.equals(int.class)) {
            setValue(((Integer) value).intValue());
            this.type = type;
        }
        else if (type.equals(Integer.class)) {
            setValue((Integer) value);
            this.type = type;
        }
        else if (type.equals(long.class)) {
            setValue(((Long) value).longValue());
            this.type = type;
        }
        else if (type.equals(Long.class)) {
            setValue((Long) value);
            this.type = type;
        }
        else if (type.equals(Date.class)) {
            setValue((Date) value);
            this.type = type;
        }
        else {
            throw new IllegalArgumentException(String.format(SR.TYPE_NOT_SUPPORTED, type.toString()));
        }
    }

    /**
     * Reserved for internal use. Constructs an {@link EntityProperty} instance from a String value and a
     * data type, and verifies that the value can be interpreted as the specified data type.
     *
     * @param value
     *            The String representation of the value to construct.
     * @param edmType
     *            The {@link EdmType} data type of the value to construct.
     */
    protected EntityProperty(final String value, final EdmType edmType) {
        this.edmType = edmType;
        this.value = value;

        // validate data is encoded correctly
        if (edmType == EdmType.STRING) {
            this.type = String.class;
            return;
        }
        else if (edmType == EdmType.BINARY) {
            this.getValueAsByteArray();
            this.type = Byte[].class;
        }
        else if (edmType == EdmType.BOOLEAN) {
            this.getValueAsBoolean();
            this.type = Boolean.class;
        }
        else if (edmType == EdmType.DOUBLE) {
            this.getValueAsDouble();
            this.type = Double.class;
        }
        else if (edmType == EdmType.GUID) {
            this.getValueAsUUID();
            this.type = UUID.class;
        }
        else if (edmType == EdmType.INT32) {
            this.getValueAsInteger();
            this.type = Integer.class;
        }
        else if (edmType == EdmType.INT64) {
            this.getValueAsLong();
            this.type = Long.class;
        }
        else if (edmType == EdmType.DATE_TIME) {
            this.getValueAsDate();
            this.type = Date.class;
        }
        else {
            // these are overwritten when this is called from the table parser with a more informative message
            if (edmType == null) {
                throw new IllegalArgumentException(SR.EDMTYPE_WAS_NULL);
            }
            throw new IllegalArgumentException(String.format(SR.INVALID_EDMTYPE_VALUE, edmType.toString()));
        }
    }

    /**
     * Constructs an {@link EntityProperty} instance from a boolean value.
     *
     * @param value
     *            The boolean value of the entity property to set.
     */
    public EntityProperty(final boolean value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a Boolean value.
     *
     * @param value
     *            The Boolean value of the entity property to set.
     */
    public EntityProperty(final Boolean value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a byte[] value.
     *
     * @param value
     *            The byte[] value of the entity property to set.
     */
    public EntityProperty(final byte[] value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a Byte[].
     *
     * @param value
     *            The Byte[] to set as the entity property value.
     */
    public EntityProperty(final Byte[] value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a java.util.Date value.
     *
     * @param value
     *            The java.util.Date to set as the entity property value.
     */
    public EntityProperty(final Date value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a double value.
     *
     * @param value
     *            The double value of the entity property to set.
     */
    public EntityProperty(final double value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a Double value.
     *
     * @param value
     *            The Double value of the entity property to set.
     */
    public EntityProperty(final Double value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from an int value.
     *
     * @param value
     *            The int value of the entity property to set.
     */
    public EntityProperty(final int value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from an Integer value.
     *
     * @param value
     *            The Integer value of the entity property to set.
     */
    public EntityProperty(final Integer value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a long value.
     *
     * @param value
     *            The long value of the entity property to set.
     */
    public EntityProperty(final long value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a Long value.
     *
     * @param value
     *            The Long value of the entity property to set.
     */
    public EntityProperty(final Long value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a String value.
     *
     * @param value
     *            The String to set as the entity property value.
     */
    public EntityProperty(final String value) {
        this.setValue(value);
    }

    /**
     * Constructs an {@link EntityProperty} instance from a java.util.UUID value.
     *
     * @param value
     *            The java.util.UUID to set as the entity property value.
     */
    public EntityProperty(final UUID value) {
        this.setValue(value);
    }

    /**
     * Gets the {@link EdmType} storage data type for the {@link EntityProperty}.
     *
     * @return
     *         The {@link EdmType} enumeration value for the data type of the {@link EntityProperty}.
     */
    public EdmType getEdmType() {
        return this.edmType;
    }

    /**
     * Gets a flag indicating that the {@link EntityProperty} value is null.
     *
     * @return
     *         A boolean flag indicating that the {@link EntityProperty} value is null.
     */
    public boolean getIsNull() {
        return this.value == null;
    }

    /**
     * Gets the class type of the {@link EntityProperty}.
     *
     * @return
     *         The Class<?> of the {@link EntityProperty}.
     */
    public Class<?> getType() {
        return this.type;
    }

    /**
     * Gets the value of this {@link EntityProperty} as a boolean.
     *
     * @return
     *         A boolean representation of the {@link EntityProperty} value.
     *
     * @throws IllegalArgumentException
     *             If the value is null or cannot be parsed as a Boolean.
     */
    public boolean getValueAsBoolean() {
        if (this.getIsNull()) {
            throw new IllegalArgumentException(SR.ENTITY_PROPERTY_CANNOT_BE_NULL_FOR_PRIMITIVES);
        }
        return Boolean.parseBoolean(this.value);
    }

    /**
     * Gets the value of this {@link EntityProperty} as a Boolean.
     *
     * @return
     *         A Boolean representation of the {@link EntityProperty} value.
     *
     * @throws IllegalArgumentException
     *             If the value is null or cannot be parsed as a Boolean.
     */
    public Boolean getValueAsBooleanObject() {
        if (this.getIsNull()) {
            return null;
        }
        return Boolean.parseBoolean(this.value);
    }

    /**
     * Gets the value of this {@link EntityProperty} as a byte array.
     *
     * @return
     *         A byte[] representation of the {@link EntityProperty} value, or null.
     */
    public byte[] getValueAsByteArray() {
        return this.getIsNull() ? null : Base64.decode(this.value);
    }

    /**
     * Gets the value of this {@link EntityProperty} as a Byte array.
     *
     * @return
     *         A Byte[] representation of the {@link EntityProperty} value, or null.
     */
    public Byte[] getValueAsByteObjectArray() {
        return this.getIsNull() ? null : Base64.decodeAsByteObjectArray(this.value);
    }

    /**
     * Gets the value of this {@link EntityProperty} as a java.util.Date.
     *
     * @return
     *         A java.util.Date representation of the {@link EntityProperty} value, or null.
     *
     * @throws IllegalArgumentException
     *             If the value is not null and cannot be parsed as a java.util.Date.
     */
    public Date getValueAsDate() {
        return this.getIsNull() ? null : Utility.parseDate(this.value, this.dateBackwardCompatibility);
    }

    /**
     * Gets the value of this {@link EntityProperty} as a double.
     *
     * @return
     *         A double representation of the {@link EntityProperty} value.
     *
     * @throws IllegalArgumentException
     *             If the value is null or cannot be parsed as a double.
     */
    public double getValueAsDouble() {
        if (this.getIsNull()) {
            throw new IllegalArgumentException(SR.ENTITY_PROPERTY_CANNOT_BE_NULL_FOR_PRIMITIVES);
        }
        else if (this.value.equals("Infinity") || this.value.equals("INF")) {
            return Double.POSITIVE_INFINITY;
        }
        else if (this.value.equals("-Infinity") || this.value.equals("-INF")) {
            return Double.NEGATIVE_INFINITY;
        }
        else if (this.value.equals("NaN")) {
            return Double.NaN;
        }
        else {
            return Double.parseDouble(this.value);
        }
    }

    /**
     * Gets the value of this {@link EntityProperty} as a double.
     *
     * @return
     *         A double representation of the {@link EntityProperty} value.
     *
     * @throws IllegalArgumentException
     *             If the value is null or cannot be parsed as a double.
     */
    public Double getValueAsDoubleObject() {
        if (this.getIsNull()) {
            return null;
        }
        else if (this.value.equals("Infinity") || this.value.equals("INF")) {
            return Double.POSITIVE_INFINITY;
        }
        else if (this.value.equals("-Infinity") || this.value.equals("-INF")) {
            return Double.NEGATIVE_INFINITY;
        }
        else if (this.value.equals("NaN")) {
            return Double.NaN;
        }
        else {
            return Double.parseDouble(this.value);
        }
    }

    /**
     * Gets the value of this {@link EntityProperty} as an int.
     *
     * @return
     *         An int representation of the {@link EntityProperty} value.
     *
     * @throws IllegalArgumentException
     *             If the value is null or cannot be parsed as an int.
     */
    public int getValueAsInteger() {
        if (this.getIsNull()) {
            throw new IllegalArgumentException(SR.ENTITY_PROPERTY_CANNOT_BE_NULL_FOR_PRIMITIVES);
        }
        return Integer.parseInt(this.value);
    }

    /**
     * Gets the value of this {@link EntityProperty} as an Integer.
     *
     * @return
     *         An Integer representation of the {@link EntityProperty} value.
     *
     * @throws IllegalArgumentException
     *             If the value is null or cannot be parsed as an int.
     */
    public Integer getValueAsIntegerObject() {
        return this.getIsNull() ? null : Integer.parseInt(this.value);
    }

    /**
     * Gets the value of this {@link EntityProperty} as a long.
     *
     * @return
     *         A long representation of the {@link EntityProperty} value.
     *
     * @throws IllegalArgumentException
     *             If the value is null or cannot be parsed as a long.
     */
    public long getValueAsLong() {
        if (this.getIsNull()) {
            throw new IllegalArgumentException(SR.ENTITY_PROPERTY_CANNOT_BE_NULL_FOR_PRIMITIVES);
        }
        return Long.parseLong(this.value);
    }

    /**
     * Gets the value of this {@link EntityProperty} as a Long.
     *
     * @return
     *         A long representation of the {@link EntityProperty} value.
     *
     * @throws IllegalArgumentException
     *             If the value is null or cannot be parsed as a long.
     */
    public Long getValueAsLongObject() {
        return this.getIsNull() ? null : Long.parseLong(this.value);
    }

    /**
     * Gets the value of this {@link EntityProperty} as a String.
     *
     * @return
     *         A String representation of the {@link EntityProperty} value, or null.
     */
    public String getValueAsString() {
        return this.getIsNull() ? null : this.value;
    }

    /**
     * Gets the value of this {@link EntityProperty} as a java.util.UUID.
     *
     * @return
     *         A java.util.UUID representation of the {@link EntityProperty} value, or null.
     *
     * @throws IllegalArgumentException
     *             If the value cannot be parsed as a java.util.UUID.
     */
    public UUID getValueAsUUID() {
        return this.getIsNull() ? null : UUID.fromString(this.value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized boolean value.
     *
     * @param value
     *            The boolean value to set as the {@link EntityProperty} value.
     */
    public synchronized final void setValue(final boolean value) {
        this.edmType = EdmType.BOOLEAN;
        this.type = boolean.class;
        this.value = value ? Constants.TRUE : Constants.FALSE;
    }

    /**
     * Sets this {@link EntityProperty} using the serialized Boolean value.
     *
     * @param value
     *            The Boolean value to set as the {@link EntityProperty} value.
     */
    public synchronized final void setValue(final Boolean value) {
        this.edmType = EdmType.BOOLEAN;
        this.type = Boolean.class;
        if (value == null) {
            this.value = null;
        }
        else {
            this.value = value ? Constants.TRUE : Constants.FALSE;
        }
    }

    /**
     * Sets this {@link EntityProperty} using the serialized byte[] value.
     *
     * @param value
     *            The byte[] value to set as the {@link EntityProperty} value. This value may be
     *            null.
     */
    public synchronized final void setValue(final byte[] value) {
        this.edmType = EdmType.BINARY;
        this.type = byte[].class;
        this.value = value == null ? null : Base64.encode(value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized Byte[] value.
     *
     * @param value
     *            The Byte[] value to set as the {@link EntityProperty} value. This value may be
     *            null.
     */
    public synchronized final void setValue(final Byte[] value) {
        this.edmType = EdmType.BINARY;
        this.type = Byte[].class;
        this.value = value == null ? null : Base64.encode(value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized java.util.Date value.
     *
     * @param value
     *            The java.util.Date value to set as the {@link EntityProperty} value. This value may be
     *            null.
     */
    public synchronized final void setValue(final Date value) {
        this.edmType = EdmType.DATE_TIME;
        this.type = Date.class;
        this.value = value == null ? null : Utility.getJavaISO8601Time(value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized double value.
     *
     * @param value
     *            The double value to set as the {@link EntityProperty} value.
     */
    public synchronized final void setValue(final double value) {
        this.edmType = EdmType.DOUBLE;
        this.type = double.class;
        this.value = Double.toString(value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized Double value.
     *
     * @param value
     *            The Double value to set as the {@link EntityProperty} value.
     */
    public synchronized final void setValue(final Double value) {
        this.edmType = EdmType.DOUBLE;
        this.type = Double.class;
        this.value = value == null ? null : Double.toString(value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized int value.
     *
     * @param value
     *            The int value to set as the {@link EntityProperty} value.
     */
    public synchronized final void setValue(final int value) {
        this.edmType = EdmType.INT32;
        this.type = int.class;
        this.value = Integer.toString(value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized Integer value.
     *
     * @param value
     *            The Integer value to set as the {@link EntityProperty} value.
     */
    public synchronized final void setValue(final Integer value) {
        this.edmType = EdmType.INT32;
        this.type = Integer.class;
        this.value = value == null ? null : Integer.toString(value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized long value.
     *
     * @param value
     *            The long value to set as the {@link EntityProperty} value.
     */
    public synchronized final void setValue(final long value) {
        this.edmType = EdmType.INT64;
        this.type = long.class;
        this.value = Long.toString(value);
    }

    /**
     * Sets this {@link EntityProperty} using the serialized Long value.
     *
     * @param value
     *            The Long value to set as the {@link EntityProperty} value.
     */
    public synchronized final void setValue(final Long value) {
        this.edmType = EdmType.INT64;
        this.type = Long.class;
        this.value = value == null ? null : Long.toString(value);
    }

    /**
     * Sets this {@link EntityProperty} using the String value.
     *
     * @param value
     *            The String value to set as the {@link EntityProperty} value. This value may be
     *            null.
     */
    public synchronized final void setValue(final String value) {
        this.edmType = EdmType.STRING;
        this.type = String.class;
        this.value = value;
    }

    /**
     * Sets this {@link EntityProperty} using the serialized java.util.UUID value.
     *
     * @param value
     *            The java.util.UUID value to set as the {@link EntityProperty} value.
     *            This value may be null.
     */
    public synchronized final void setValue(final UUID value) {
        this.edmType = EdmType.GUID;
        this.type = UUID.class;
        this.value = value == null ? null : value.toString();
    }

    /**
     * Sets whether the client should look to correct Date values stored on a {@link TableEntity}
     * that may have been written using versions of this library prior to 0.4.0.
     *

     * {@link #dateBackwardCompatibility} is by default false, indicating a post 0.4.0 version or
     * mixed-platform usage.
     *

     * See here for more details.
     *
     * @param dateBackwardCompatibility
     *        true to enable dateBackwardCompatibility; otherwise, false
     */
    void setDateBackwardCompatibility(boolean dateBackwardCompatibility) {
        this.dateBackwardCompatibility = dateBackwardCompatibility;
    }
}


/**
 * 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.util.concurrent.CopyOnWriteArrayList;

/**
 * Represents a generic event multi-caster that allows event listeners to be dynamically added and removed.
 *
 * @param <EVENT_TYPE>
 *            An object that represents the type of the event.
 * @param <EVENT_LISTENER_TYPE>
 *            An object that represents the type of the event listener.
 */
public final class StorageEventMultiCaster<EVENT_TYPE extends BaseEvent, EVENT_LISTENER_TYPE extends StorageEvent<EVENT_TYPE>> {

    /**
     * Holds the list of listeners.
     */
    private final CopyOnWriteArrayList<EVENT_LISTENER_TYPE> listeners = new CopyOnWriteArrayList<EVENT_LISTENER_TYPE>();

    /**
     * Adds a listener to the event chain.
     *
     * @param listener
     *            An EventListenerType object that represents the listener to add.
     */
    public void addListener(final EVENT_LISTENER_TYPE listener) {
        this.listeners.add(listener);
    }

    /**
     * Fires the event to all subscribed event listeners.
     *
     * @param event
     *            An EVENTTYPEobject that represents the event being multi-casted.
     */
    public void fireEvent(final EVENT_TYPE event) {
        for (final StorageEvent<EVENT_TYPE> listener : this.listeners) {
            listener.eventOccurred(event);
        }
    }

    /**
     * Returns a value that indicates whether any event listeners are registered for events.
     *
     * @return true if any event listeners are registered; otherwise, false.
     */
    public boolean hasListeners() {
        return this.listeners.size() > 0;
    }

    /**
     * Removes an event listener from the event chain.
     *
     * @param listener
     *            An EventListenerType object that represents the listener to remove.
     */
    public void removeListener(final EVENT_LISTENER_TYPE listener) {
        this.listeners.remove(listener);
    }
}


/**
 * 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.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.Date;
import java.util.Map.Entry;
import java.util.concurrent.TimeoutException;
import android.os.NetworkOnMainThreadException;
import com.microsoft.azure.storage.*;

/**
 * RESERVED FOR INTERNAL USE. A class that handles execution of StorageOperations and enforces retry policies.
 */
public final class ExecutionEngine {
    /**
     * Executes an operation and enforces a retrypolicy to handle any potential errors
     *
     * @param <CLIENT_TYPE>
     *            The type of the service client
     * @param <PARENT_TYPE>
     *            The type of the parent object, i.e. CloudBlobContainer for downloadAttributes etc.
     * @param <RESULT_TYPE>
     *            The type of the expected result
     * @param client
     *            the service client associated with the request
     * @param parentObject
     *            the parent object
     * @param task
     *            the StorageRequest to execute
     * @param policyFactory
     *            the factory used to generate a new retry policy instance
     * @param opContext
     *            an object used to track the execution of the operation
     * @return the result of the operation
     * @throws StorageException
     *             an exception representing any error which occurred during the operation.
     */
    public static <CLIENT_TYPE, PARENT_TYPE, RESULT_TYPE> RESULT_TYPE executeWithRetry(final CLIENT_TYPE client,
            final PARENT_TYPE parentObject, final StorageRequest<CLIENT_TYPE, PARENT_TYPE, RESULT_TYPE> task,
            final RetryPolicyFactory policyFactory, final OperationContext opContext) throws StorageException {

        RetryPolicy policy = null;

        if (policyFactory == null) {
            policy = new RetryNoRetry();
        }
        else {
            policy = policyFactory.createInstance(opContext);

            // if the returned policy is null, set to not retry
            if (policy == null) {
                policy = new RetryNoRetry();
            }
        }

        int currentRetryCount = 0;
        StorageException translatedException = null;
        HttpURLConnection request = null;
        final long startTime = new Date().getTime();

        while (true) {
            try {
                // 1-4: setup the request
                request = setupStorageRequest(client, parentObject, task, currentRetryCount, opContext);

                Logger.info(opContext, LogConstants.START_REQUEST, request.getURL(),
                        request.getRequestProperty(Constants.HeaderConstants.DATE));

                // 5. Potentially upload data
                boolean responseReceivedEventTriggered = false;
                try {
                    if (task.getSendStream() != null) {
                        Logger.info(opContext, LogConstants.UPLOAD);
                        final StreamMd5AndLength descriptor = Utility.writeToOutputStream(task.getSendStream(),
                                request.getOutputStream(), task.getLength(), false /* rewindStream */,
                                false /* calculate MD5 */, opContext, task.getRequestOptions());

                        task.validateStreamWrite(descriptor);
                        Logger.info(opContext, LogConstants.UPLOADDONE);
                    }

                    Utility.logHttpRequest(request, opContext);

                    // 6. Process the request - Get response
                    RequestResult currResult = task.getResult();
                    currResult.setStartDate(new Date());

                    Logger.info(opContext, LogConstants.GET_RESPONSE);

                    currResult.setStatusCode(request.getResponseCode());
                    currResult.setStatusMessage(request.getResponseMessage());
                    currResult.setStopDate(new Date());

                    currResult.setServiceRequestID(BaseResponse.getRequestId(request));
                    currResult.setEtag(BaseResponse.getEtag(request));
                    currResult.setRequestDate(BaseResponse.getDate(request));
                    currResult.setContentMD5(BaseResponse.getContentMD5(request));

                    // 7. Fire ResponseReceived Event
                    responseReceivedEventTriggered = true;
                    ExecutionEngine.fireResponseReceivedEvent(opContext, request, task.getResult());

                    Logger.info(opContext, LogConstants.RESPONSE_RECEIVED, currResult.getStatusCode(),
                            currResult.getServiceRequestID(), currResult.getContentMD5(), currResult.getEtag(),
                            currResult.getRequestDate());

                    Utility.logHttpResponse(request, opContext);
                }
                finally {
                    Logger.info(opContext, LogConstants.ERROR_RECEIVING_RESPONSE);
                    if (!responseReceivedEventTriggered) {
                        if (task.getResult().getStartDate() == null) {
                            task.getResult().setStartDate(new Date());
                        }

                        ExecutionEngine.fireErrorReceivingResponseEvent(opContext, request, task.getResult());
                    }
                }

                // 8. Pre-process response to check if there was an exception. Do Response parsing (headers etc).
                Logger.info(opContext, LogConstants.PRE_PROCESS);
                RESULT_TYPE result = task.preProcessResponse(parentObject, client, opContext);
                Logger.info(opContext, LogConstants.PRE_PROCESS_DONE);

                if (!task.isNonExceptionedRetryableFailure()) {

                    // 9. Post-process response. Read stream from server.
                    Logger.info(opContext, LogConstants.POST_PROCESS);
                    result = task.postProcessResponse(request, parentObject, client, opContext, result);
                    Logger.info(opContext, LogConstants.POST_PROCESS_DONE);

                    // Success return result and drain the input stream.
                    if ((task.getResult().getStatusCode() >= 200) && (task.getResult().getStatusCode() < 300)) {
                        if (request != null) {
                            InputStream inStream = request.getInputStream();
                            // At this point, we already have a result / exception to return to the user.
                            // This is just an optimization to improve socket reuse.
                            try {
                                Utility.writeToOutputStream(inStream, null, -1, false, false, null,
                                        task.getRequestOptions());
                            }
                            catch (final IOException ex) {
                            }
                            catch (StorageException e) {
                            }
                            finally {
                                inStream.close();
                            }
                        }
                    }
                    Logger.info(opContext, LogConstants.COMPLETE);

                    return result;
                }
                else {
                    Logger.warn(opContext, LogConstants.UNEXPECTED_RESULT_OR_EXCEPTION);
                    // The task may have already parsed an exception.
                    translatedException = task.materializeException(opContext);
                    task.getResult().setException(translatedException);

                    // throw on non retryable status codes: 501, 505, blob type mismatch
                    if (task.getResult().getStatusCode() == HttpURLConnection.HTTP_NOT_IMPLEMENTED
                            || task.getResult().getStatusCode() == HttpURLConnection.HTTP_VERSION
                            || translatedException.getErrorCode().equals(StorageErrorCodeStrings.INVALID_BLOB_TYPE)) {
                        throw translatedException;
                    }
                }
            }
            catch (final NetworkOnMainThreadException e) {
                // Non Retryable, just throw
                translatedException = new StorageException("NetworkOnMainThreadException",
                        SR.NETWORK_ON_MAIN_THREAD_EXCEPTION, -1, null, e);
                task.getResult().setException(translatedException);
                Logger.error(opContext, LogConstants.UNRETRYABLE_EXCEPTION, e.getClass().getName(), e.getMessage());
                throw translatedException;
            }
            catch (final StorageException e) {
                // In case of table batch error or internal error, the exception will contain a different
                // status code and message than the original HTTP response. Reset based on error values.
                task.getResult().setStatusCode(e.getHttpStatusCode());
                task.getResult().setStatusMessage(e.getMessage());
                task.getResult().setException(e);

                Logger.warn(opContext, LogConstants.RETRYABLE_EXCEPTION, e.getClass().getName(), e.getMessage());
                translatedException = e;
            }
            catch (final Exception e) {
                // Retryable, wrap
                Logger.warn(opContext, LogConstants.RETRYABLE_EXCEPTION, e.getClass().getName(), e.getMessage());
                translatedException = StorageException.translateException(task, e, opContext);
                task.getResult().setException(translatedException);
            }
            finally {
                opContext.setClientTimeInMs(new Date().getTime() - startTime);

                // 10. Fire RequestCompleted Event
                if (task.isSent()) {
                    ExecutionEngine.fireRequestCompletedEvent(opContext, request, task.getResult());
                }
            }

            // Evaluate Retry Policy
            Logger.info(opContext, LogConstants.RETRY_CHECK, currentRetryCount, task.getResult().getStatusCode(),
                    translatedException == null ? null : translatedException.getMessage());

            task.setCurrentLocation(getNextLocation(task.getCurrentLocation(), task.getLocationMode()));
            Logger.info(opContext, LogConstants.NEXT_LOCATION, task.getCurrentLocation(), task.getLocationMode());

            RetryContext retryContext = new RetryContext(currentRetryCount++, task.getResult(),
                    task.getCurrentLocation(), task.getLocationMode());

            RetryInfo retryInfo = policy.evaluate(retryContext, opContext);

            if (retryInfo == null) {
                // policy does not allow for retry
                Logger.error(opContext, LogConstants.DO_NOT_RETRY_POLICY, translatedException == null ? null
                        : translatedException.getMessage());
                throw translatedException;
            }
            else if (Utility.validateMaxExecutionTimeout(task.getRequestOptions().getOperationExpiryTimeInMs(),
                    retryInfo.getRetryInterval())) {
                // maximum execution time would be exceeded by current time plus retry interval delay
                TimeoutException timeoutException = new TimeoutException(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION);
                translatedException = new StorageException(StorageErrorCodeStrings.OPERATION_TIMED_OUT,
                        SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION, Constants.HeaderConstants.HTTP_UNUSED_306, null,
                        timeoutException);

                task.initialize(opContext);
                task.getResult().setException(translatedException);

                Logger.error(opContext, LogConstants.DO_NOT_RETRY_TIMEOUT, translatedException == null ? null
                        : translatedException.getMessage());

                throw translatedException;
            }
            else {
                // attempt to retry
                task.setCurrentLocation(retryInfo.getTargetLocation());
                task.setLocationMode(retryInfo.getUpdatedLocationMode());
                Logger.info(opContext, LogConstants.RETRY_INFO, task.getCurrentLocation(), task.getLocationMode());

                try {
                    ExecutionEngine.fireRetryingEvent(opContext, task.getConnection(), task.getResult(), retryContext);

                    Logger.info(opContext, LogConstants.RETRY_DELAY, retryInfo.getRetryInterval());
                    Thread.sleep(retryInfo.getRetryInterval());
                }
                catch (final InterruptedException e) {
                    // Restore the interrupted status
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    private static <CLIENT_TYPE, PARENT_TYPE, RESULT_TYPE> HttpURLConnection setupStorageRequest(
            final CLIENT_TYPE client, final PARENT_TYPE parentObject,
            final StorageRequest<CLIENT_TYPE, PARENT_TYPE, RESULT_TYPE> task, int currentRetryCount,
            final OperationContext opContext) throws StorageException {
        try {

            // reset result flags
            task.initialize(opContext);

            if (Utility.validateMaxExecutionTimeout(task.getRequestOptions().getOperationExpiryTimeInMs())) {
                // maximum execution time would be exceeded by current time
                TimeoutException timeoutException = new TimeoutException(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION);
                throw new StorageException(StorageErrorCodeStrings.OPERATION_TIMED_OUT,
                        SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION, Constants.HeaderConstants.HTTP_UNUSED_306, null,
                        timeoutException);
            }

            // Run the recovery action if this is a retry. Else, initialize the location mode for the task.
            // For retries, it will be initialized in retry logic.
            if (currentRetryCount > 0) {
                task.recoveryAction(opContext);
                Logger.info(opContext, LogConstants.RETRY);
            }
            else {
                task.applyLocationModeToRequest();
                task.initializeLocation();
                Logger.info(opContext, LogConstants.STARTING);
            }

            task.setRequestLocationMode();

            // If the command only allows for a specific location, we should target
            // that location no matter what the retry policy says.
            task.validateLocation();

            Logger.info(opContext, LogConstants.INIT_LOCATION, task.getCurrentLocation(), task.getLocationMode());

            // 1. Build the request
            HttpURLConnection request = task.buildRequest(client, parentObject, opContext);

            // 2. Add headers
            task.setHeaders(request, parentObject, opContext);

            // Add any other custom headers that users have set on the opContext
            if (opContext.getUserHeaders() != null) {
                for (final Entry<String, String> entry : opContext.getUserHeaders().entrySet()) {
                    request.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }

            // 3. Fire sending request event
            ExecutionEngine.fireSendingRequestEvent(opContext, request, task.getResult());
            task.setIsSent(true);

            // 4. Sign the request
            task.signRequest(request, client, opContext);

            // set the connection on the task
            task.setConnection(request);

            return request;
        }
        catch (StorageException e) {
            throw e;
        }
        catch (Exception e) {
            throw new StorageException(null, e.getMessage(), Constants.HeaderConstants.HTTP_UNUSED_306, null, e);
        }
    }

    private static StorageLocation getNextLocation(StorageLocation lastLocation, LocationMode locationMode) {
        switch (locationMode) {
            case PRIMARY_ONLY:
                return StorageLocation.PRIMARY;

            case SECONDARY_ONLY:
                return StorageLocation.SECONDARY;

            case PRIMARY_THEN_SECONDARY:
            case SECONDARY_THEN_PRIMARY:
                return (lastLocation == StorageLocation.PRIMARY) ? StorageLocation.SECONDARY : StorageLocation.PRIMARY;

            default:
                return StorageLocation.PRIMARY;
        }
    }

    /**
     * Fires events representing that a request will be sent.
     */
    private static void fireSendingRequestEvent(OperationContext opContext, HttpURLConnection request,
            RequestResult result) {
        if (opContext.getSendingRequestEventHandler().hasListeners()
                || OperationContext.getGlobalSendingRequestEventHandler().hasListeners()) {
            SendingRequestEvent event = new SendingRequestEvent(opContext, request, result);
            opContext.getSendingRequestEventHandler().fireEvent(event);
            OperationContext.getGlobalSendingRequestEventHandler().fireEvent(event);
        }
    }

    /**
     * Fires events representing that a response has been received.
     */
    private static void fireResponseReceivedEvent(OperationContext opContext, HttpURLConnection request,
            RequestResult result) {
        if (opContext.getResponseReceivedEventHandler().hasListeners()
                || OperationContext.getGlobalResponseReceivedEventHandler().hasListeners()) {
            ResponseReceivedEvent event = new ResponseReceivedEvent(opContext, request, result);
            opContext.getResponseReceivedEventHandler().fireEvent(event);
            OperationContext.getGlobalResponseReceivedEventHandler().fireEvent(event);
        }
    }

    /**
     * Fires events representing that an error occurred when receiving the response.
     */
    private static void fireErrorReceivingResponseEvent(OperationContext opContext, HttpURLConnection request,
            RequestResult result) {
        if (opContext.getErrorReceivingResponseEventHandler().hasListeners()
                || OperationContext.getGlobalErrorReceivingResponseEventHandler().hasListeners()) {
            ErrorReceivingResponseEvent event = new ErrorReceivingResponseEvent(opContext, request, result);
            opContext.getErrorReceivingResponseEventHandler().fireEvent(event);
            OperationContext.getGlobalErrorReceivingResponseEventHandler().fireEvent(event);
        }
    }

    /**
     * Fires events representing that a response received from the service is fully processed.
     */
    private static void fireRequestCompletedEvent(OperationContext opContext, HttpURLConnection request,
            RequestResult result) {
        if (opContext.getRequestCompletedEventHandler().hasListeners()
                || OperationContext.getGlobalRequestCompletedEventHandler().hasListeners()) {
            RequestCompletedEvent event = new RequestCompletedEvent(opContext, request, result);
            opContext.getRequestCompletedEventHandler().fireEvent(event);
            OperationContext.getGlobalRequestCompletedEventHandler().fireEvent(event);
        }
    }

    /**
     * Fires events representing that a request will be retried.
     */
    private static void fireRetryingEvent(OperationContext opContext, HttpURLConnection request, RequestResult result,
            RetryContext retryContext) {
        if (opContext.getRetryingEventHandler().hasListeners()
                || OperationContext.getGlobalRetryingEventHandler().hasListeners()) {
            RetryingEvent event = new RetryingEvent(opContext, request, result, retryContext);
            opContext.getRetryingEventHandler().fireEvent(event);
            OperationContext.getGlobalRetryingEventHandler().fireEvent(event);
        }
    }
}


/**
 * 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.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

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

/**
 * RESERVED FOR INTERNAL USE. A class used to deserialize service stats.
 */
final class ServiceStatsHandler extends DefaultHandler {
    /**
     * The name of the geo-replication XML element.
     */
    private final static String GEO_REPLICATION_NAME = "GeoReplication";

    /**
     * The name of the status XML element.
     */
    private final static String STATUS_NAME = "Status";

    /**
     * The name of the last sync time XML element.
     */
    private final static String LAST_SYNC_TIME_NAME = "LastSyncTime";

    private final Stack<String> elementStack = new Stack<String>();
    private StringBuilder bld = new StringBuilder();

    private final GeoReplicationStats geoReplicationStats = new GeoReplicationStats();
    private final ServiceStats stats = new ServiceStats();

    /**
     * Constructs a {@link ServiceStats} object from an XML document received from the service.
     *
     * @param inStream
     *            The XMLStreamReader object.
     * @return
     *         A {@link ServiceStats} object containing the properties in the XML document.
     *
     * @throws SAXException
     * @throws ParserConfigurationException
     * @throws IOException
     */
    public static ServiceStats readServiceStatsFromStream(final InputStream inStream)
            throws ParserConfigurationException, SAXException, IOException {
        SAXParser saxParser = Utility.getSAXParser();
        ServiceStatsHandler handler = new ServiceStatsHandler();
        saxParser.parse(inStream, handler);

        return handler.stats;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        this.elementStack.push(localName);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        String currentNode = this.elementStack.pop();

        // if the node popped from the stack and the localName don't match, the xml document is improperly formatted
        if (!localName.equals(currentNode)) {
            throw new SAXException(SR.INVALID_RESPONSE_RECEIVED);
        }

        String value = this.bld.toString();
        if (value.isEmpty()) {
            value = null;
        }

        if (GEO_REPLICATION_NAME.equals(currentNode)) {
            this.stats.setGeoReplication(this.geoReplicationStats);
        }
        else if (STATUS_NAME.equals(currentNode)) {
            this.geoReplicationStats.setStatus(GeoReplicationStatus.parse(value));
        }
        else if (LAST_SYNC_TIME_NAME.equals(currentNode)) {
            try {
                this.geoReplicationStats.setLastSyncTime(Utility.isNullOrEmpty(value) ? null : Utility
                        .parseRFC1123DateFromStringInGMT(value));
            }
            catch (ParseException e) {
                throw new SAXException(e);
            }
        }

        this.bld = new StringBuilder();
    }

    @Override
    public void characters(char ch[], int start, int length) throws SAXException {
        this.bld.append(ch, start, length);
    }

}