/**
 * 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.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.concurrent.TimeoutException;

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

import org.xml.sax.SAXException;
import org.xmlpull.v1.XmlSerializer;

import android.util.Log;
import android.util.Xml;

import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.RequestOptions;
import com.microsoft.azure.storage.ResultContinuation;
import com.microsoft.azure.storage.ResultContinuationType;
import com.microsoft.azure.storage.StorageErrorCode;
import com.microsoft.azure.storage.StorageErrorCodeStrings;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.StorageExtendedErrorInformation;

/**
 * RESERVED FOR INTERNAL USE. A class which provides utility methods.
 */
public final class Utility {

    /**
     * Stores a reference to the GMT time zone.
     */
    public static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");

    /**
     * Stores a reference to the UTC time zone.
     */
    public static final TimeZone UTC_ZONE = TimeZone.getTimeZone("UTC");

    /**
     * Stores a reference to the US locale.
     */
    public static final Locale LOCALE_US = Locale.US;

    /**
     * Stores a reference to the RFC1123 date/time pattern.
     */
    private static final String RFC1123_GMT_PATTERN = "EEE, dd MMM yyyy HH:mm:ss 'GMT'";

    /**
     * Stores a reference to the ISO8601 date/time pattern.
     */
    private static final String ISO8601_PATTERN_NO_SECONDS = "yyyy-MM-dd'T'HH:mm'Z'";

    /**
     * Stores a reference to the ISO8601 date/time pattern.
     */
    private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'";

    /**
     * Stores a reference to the Java version of ISO8601_LONG date/time pattern.  The full version cannot be used
     * because Java Dates have millisecond precision.
     */
    private static final String JAVA_ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

    /**
     * List of ports used for path style addressing.
     */
    private static final List<Integer> pathStylePorts = Arrays.asList(10000, 10001, 10002, 10003, 10004, 10100, 10101,
            10102, 10103, 10104, 11000, 11001, 11002, 11003, 11004, 11100, 11101, 11102, 11103, 11104);

    /**
     * A factory to create SAXParser instances.
     */
    private static final SAXParserFactory factory = SAXParserFactory.newInstance();

    /**
     * Stores a reference to the date/time pattern with the greatest precision Java.util.Date is capable of expressing.
     */
    private static final String MAX_PRECISION_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";

    /**
     * The length of a datestring that matches the MAX_PRECISION_PATTERN.
     */
    private static final int MAX_PRECISION_DATESTRING_LENGTH = MAX_PRECISION_PATTERN.replaceAll("'", "").length();

    /**
     *
     * Determines the size of an input stream, and optionally calculates the MD5 hash for the stream.
     *
     * @param sourceStream
     *            A InputStream object that represents the stream to measure.
     * @param writeLength
     *            The number of bytes to read from the stream.
     * @param abandonLength
     *            The number of bytes to read before the analysis is abandoned. Set this value to -1 to
     *            force the entire stream to be read. This parameter is provided to support upload thresholds.
     * @param rewindSourceStream
     *            true if the stream should be rewound after it is read; otherwise, false.
     * @param calculateMD5
     *            true if an MD5 hash will be calculated; otherwise, false.
     *
     * @return A {@link StreamMd5AndLength} object that contains the stream length, and optionally the MD5 hash.
     *
     * @throws IOException
     *             If an I/O error occurs.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static StreamMd5AndLength analyzeStream(final InputStream sourceStream, long writeLength,
            long abandonLength, final boolean rewindSourceStream, final boolean calculateMD5) throws IOException,
            StorageException {
        if (abandonLength < 0) {
            abandonLength = Long.MAX_VALUE;
        }

        if (rewindSourceStream) {
            if (!sourceStream.markSupported()) {
                throw new IllegalArgumentException(SR.INPUT_STREAM_SHOULD_BE_MARKABLE);
            }

            sourceStream.mark(Constants.MAX_MARK_LENGTH);
        }

        MessageDigest digest = null;
        if (calculateMD5) {
            try {
                digest = MessageDigest.getInstance("MD5");
            }
            catch (final NoSuchAlgorithmException e) {
                // This wont happen, throw fatal.
                throw Utility.generateNewUnexpectedStorageException(e);
            }
        }

        if (writeLength < 0) {
            writeLength = Long.MAX_VALUE;
        }

        final StreamMd5AndLength retVal = new StreamMd5AndLength();
        int count = -1;
        final byte[] retrievedBuff = new byte[Constants.BUFFER_COPY_LENGTH];

        int nextCopy = (int) Math.min(retrievedBuff.length, writeLength - retVal.getLength());
        count = sourceStream.read(retrievedBuff, 0, nextCopy);

        while (nextCopy > 0 && count != -1) {
            if (calculateMD5) {
                digest.update(retrievedBuff, 0, count);
            }
            retVal.setLength(retVal.getLength() + count);

            if (retVal.getLength() > abandonLength) {
                // Abandon operation
                retVal.setLength(-1);
                retVal.setMd5(null);
                break;
            }

            nextCopy = (int) Math.min(retrievedBuff.length, writeLength - retVal.getLength());
            count = sourceStream.read(retrievedBuff, 0, nextCopy);
        }

        if (retVal.getLength() != -1 && calculateMD5) {
            retVal.setMd5(Base64.encode(digest.digest()));
        }

        if (retVal.getLength() != -1 && writeLength > 0) {
            retVal.setLength(Math.min(retVal.getLength(), writeLength));
        }

        if (rewindSourceStream) {
            sourceStream.reset();
            sourceStream.mark(Constants.MAX_MARK_LENGTH);
        }

        return retVal;
    }

    /**
     * Asserts a continuation token is of the specified type.
     *
     * @param continuationToken
     *            A {@link ResultContinuation} object that represents the continuation token whose type is being
     *            examined.
     * @param continuationType
     *            A {@link ResultContinuationType} value that represents the continuation token type being asserted with
     *            the specified continuation token.
     */
    public static void assertContinuationType(final ResultContinuation continuationToken,
            final ResultContinuationType continuationType) {
        if (continuationToken != null) {
            if (!(continuationToken.getContinuationType() == ResultContinuationType.NONE || continuationToken
                    .getContinuationType() == continuationType)) {
                final String errorMessage = String.format(Utility.LOCALE_US, SR.UNEXPECTED_CONTINUATION_TYPE,
                        continuationToken.getContinuationType(), continuationType);
                throw new IllegalArgumentException(errorMessage);
            }
        }
    }

    /**
     * Asserts that a value is not null.
     *
     * @param param
     *            A String that represents the name of the parameter, which becomes the exception message
     *            text if the value parameter is null.
     * @param value
     *            An Object object that represents the value of the specified parameter. This is the value
     *            being asserted as not null.
     */
    public static void assertNotNull(final String param, final Object value) {
        if (value == null) {
            throw new IllegalArgumentException(String.format(Utility.LOCALE_US, SR.ARGUMENT_NULL_OR_EMPTY, param));
        }
    }

    /**
     * Asserts that the specified string is not null or empty.
     *
     * @param param
     *            A String that represents the name of the parameter, which becomes the exception message
     *            text if the value parameter is null or an empty string.
     * @param value
     *            A String that represents the value of the specified parameter. This is the value being
     *            asserted as not null and not an empty string.
     */
    public static void assertNotNullOrEmpty(final String param, final String value) {
        assertNotNull(param, value);

        if (Utility.isNullOrEmpty(value)) {
            throw new IllegalArgumentException(String.format(Utility.LOCALE_US, SR.ARGUMENT_NULL_OR_EMPTY, param));
        }
    }

    /**
     * Asserts that the specified integer is in the valid range.
     *
     * @param param
     *            A String that represents the name of the parameter, which becomes the exception message
     *            text if the value parameter is out of bounds.
     * @param value
     *            The value of the specified parameter.
     * @param min
     *            The minimum value for the specified parameter.
     * @param max
     *            The maximum value for the specified parameter.
     */
    public static void assertInBounds(final String param, final long value, final long min, final long max) {
        if (value < min || value > max) {
            throw new IllegalArgumentException(String.format(SR.PARAMETER_NOT_IN_RANGE, param, min, max));
        }
    }

    /**
     * Asserts that the specified value is greater than or equal to the min value.
     *
     * @param param
     *            A String that represents the name of the parameter, which becomes the exception message
     *            text if the value parameter is out of bounds.
     * @param value
     *            The value of the specified parameter.
     * @param min
     *            The minimum value for the specified parameter.
     */
    public static void assertGreaterThanOrEqual(final String param, final long value, final long min) {
        if (value < min) {
            throw new IllegalArgumentException(String.format(SR.PARAMETER_SHOULD_BE_GREATER_OR_EQUAL, param, min));
        }
    }

    /**
     * Returns a value representing whether the maximum execution time would be surpassed.
     *
     * @param operationExpiryTimeInMs
     *            the time the request expires
     * @return true if the maximum execution time would be surpassed; otherwise, false.
     */
    public static boolean validateMaxExecutionTimeout(Long operationExpiryTimeInMs) {
        return validateMaxExecutionTimeout(operationExpiryTimeInMs, 0);
    }

    /**
     * Returns a value representing whether the maximum execution time would be surpassed.
     *
     * @param operationExpiryTimeInMs
     *            the time the request expires
     * @param additionalInterval
     *            any additional time required from now
     * @return true if the maximum execution time would be surpassed; otherwise, false.
     */
    public static boolean validateMaxExecutionTimeout(Long operationExpiryTimeInMs, long additionalInterval) {
        if (operationExpiryTimeInMs != null) {
            long currentTime = new Date().getTime();
            return operationExpiryTimeInMs < currentTime + additionalInterval;
        }
        return false;
    }

    /**
     * Returns a value representing the remaining time before the operation expires.
     *
     * @param operationExpiryTimeInMs
     *            the time the request expires
     * @param timeoutIntervalInMs
     *            the server side timeout interval
     * @return the remaining time before the operation expires
     * @throws StorageException
     *             wraps a TimeoutException if there is no more time remaining
     */
    public static int getRemainingTimeout(Long operationExpiryTimeInMs, Integer timeoutIntervalInMs) throws StorageException {
        if (operationExpiryTimeInMs != null) {
            long remainingTime = operationExpiryTimeInMs - new Date().getTime();
            if (remainingTime > Integer.MAX_VALUE) {
                return Integer.MAX_VALUE;
            }
            else if (remainingTime > 0) {
                return (int) remainingTime;
            }
            else {
                TimeoutException timeoutException = new TimeoutException(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION);
                StorageException translatedException = new StorageException(
                        StorageErrorCodeStrings.OPERATION_TIMED_OUT, SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION,
                        Constants.HeaderConstants.HTTP_UNUSED_306, null, timeoutException);
                throw translatedException;
            }
        }
        else if (timeoutIntervalInMs != null) {
            return timeoutIntervalInMs + Constants.DEFAULT_READ_TIMEOUT;
        }
        else {
            return Constants.DEFAULT_READ_TIMEOUT;
        }
    }

    /**
     * Returns a value that indicates whether a specified URI is a path-style URI.
     *
     * @param baseURI
     *            A java.net.URI value that represents the URI being checked.
     * @return true if the specified URI is path-style; otherwise, false.
     */
    public static boolean determinePathStyleFromUri(final URI baseURI) {
        String path = baseURI.getPath();
        if (path != null && path.startsWith("/")) {
            path = path.substring(1);
        }

        // if the path is null or empty, this is not path-style
        if (Utility.isNullOrEmpty(path)) {
            return false;
        }

        // if this contains a port or has a host which is not DNS, this is path-style
        return pathStylePorts.contains(baseURI.getPort()) || !isHostDnsName(baseURI);
    }

    /**
     * Returns a boolean indicating whether the host of the specified URI is DNS.
     *
     * @param uri
     *            The URI whose host to evaluate.
     * @return true if the host is DNS; otherwise, false.
     */
    private static boolean isHostDnsName(URI uri) {
        String host = uri.getHost();
        for (int i = 0; i < host.length(); i++) {
            char hostChar = host.charAt(i);
            if (!Character.isDigit(hostChar) && !(hostChar == '.')) {
                return true;
            }
        }
        return false;
    }

    /**
     * Reads character data for the Etag element from an XML stream reader.
     *
     * @param xmlr
     *            An XMLStreamReader object that represents the source XML stream reader.
     *
     * @return A String that represents the character data for the Etag element.
     */
    public static String formatETag(final String etag) {
        if (etag.startsWith("\"") && etag.endsWith("\"")) {
            return etag;
        }
        else {
            return String.format("\"%s\"", etag);
        }
    }

    /**
     * Returns an unexpected storage exception.
     *
     * @param cause
     *            An Exception object that represents the initial exception that caused the unexpected
     *            error.
     *
     * @return A {@link StorageException} object that represents the unexpected storage exception being thrown.
     */
    public static StorageException generateNewUnexpectedStorageException(final Exception cause) {
        final StorageException exceptionRef = new StorageException(StorageErrorCode.NONE.toString(),
                "Unexpected internal storage client error.", 306, // unused
                null, null);
        exceptionRef.initCause(cause);
        return exceptionRef;
    }

    /**
     * Returns a byte array that represents the data of a long value.
     *
     * @param value
     *            The value from which the byte array will be returned.
     *
     * @return A byte array that represents the data of the specified long value.
     */
    public static byte[] getBytesFromLong(final long value) {
        final byte[] tempArray = new byte[8];

        for (int m = 0; m < 8; m++) {
            tempArray[7 - m] = (byte) ((value >> (8 * m)) & 0xFF);
        }

        return tempArray;
    }

    /**
     * Returns the current GMT date/time String using the RFC1123 pattern.
     *
     * @return A String that represents the current GMT date/time using the RFC1123 pattern.
     */
    public static String getGMTTime() {
        return getGMTTime(new Date());
    }

    /**
     * Returns the GTM date/time String for the specified value using the RFC1123 pattern.
     *
     * @param date
     *            A Date object that represents the date to convert to GMT date/time in the RFC1123
     *            pattern.
     *
     * @return A String that represents the GMT date/time for the specified value using the RFC1123
     *         pattern.
     */
    public static String getGMTTime(final Date date) {
        final DateFormat formatter = new SimpleDateFormat(RFC1123_GMT_PATTERN, LOCALE_US);
        formatter.setTimeZone(GMT_ZONE);
        return formatter.format(date);
    }

    /**
     * Returns the UTC date/time String for the specified value using Java's version of the ISO8601 pattern,
     * which is limited to millisecond precision.
     *
     * @param date
     *            A Date object that represents the date to convert to UTC date/time in Java's version
     *            of the ISO8601 pattern.
     *
     * @return A String that represents the UTC date/time for the specified value using Java's version
     *            of the ISO8601 pattern.
     */
    public static String getJavaISO8601Time(Date date) {
        final DateFormat formatter = new SimpleDateFormat(JAVA_ISO8601_PATTERN, LOCALE_US);
        formatter.setTimeZone(UTC_ZONE);
        return formatter.format(date);
    }

    /**
     * Returns a namespace aware SAXParser.
     *
     * @return A SAXParser instance which is namespace aware
     *
     * @throws ParserConfigurationException
     * @throws SAXException
     */
    public static SAXParser getSAXParser() throws ParserConfigurationException, SAXException {
        factory.setNamespaceAware(true);
        return factory.newSAXParser();
    }

    /**
     * Returns the standard header value from the specified connection request, or an empty string if no header value
     * has been specified for the request.
     *
     * @param conn
     *            An HttpURLConnection object that represents the request.
     * @param headerName
     *            A String that represents the name of the header being requested.
     *
     * @return A String that represents the header value, or null if there is no corresponding
     *         header value for headerName.
     */
    public static String getStandardHeaderValue(final HttpURLConnection conn, final String headerName) {
        final String headerValue = conn.getRequestProperty(headerName);

        // Coalesce null value
        return headerValue == null ? Constants.EMPTY_STRING : headerValue;
    }

    /**
     * Returns the UTC date/time for the specified value using the ISO8601 pattern.
     *
     * @param value
     *            A Date object that represents the date to convert to UTC date/time in the ISO8601
     *            pattern. If this value is null, this method returns an empty string.
     *
     * @return A String that represents the UTC date/time for the specified value using the ISO8601
     *         pattern, or an empty string if value is null.
     */
    public static String getUTCTimeOrEmpty(final Date value) {
        if (value == null) {
            return Constants.EMPTY_STRING;
        }

        final DateFormat iso8601Format = new SimpleDateFormat(ISO8601_PATTERN, LOCALE_US);
        iso8601Format.setTimeZone(UTC_ZONE);

        return iso8601Format.format(value);
    }

    /**
     * Returns a XmlSerializer which outputs to the specified StringWriter
     *
     * @param outWriter
     *            the StringWriter to output to
     * @return A XmlSerializer instance
     *
     * @throws IOException
     *             if there is an error setting the output.
     * @throws IllegalStateException
     *             if there is an error setting the output.
     * @throws IllegalArgumentException
     *             if there is an error setting the output.
     */
    public static XmlSerializer getXmlSerializer(StringWriter outWriter) throws IllegalArgumentException,
            IllegalStateException, IOException {
        XmlSerializer serializer = Xml.newSerializer();
        serializer.setOutput(outWriter);

        return serializer;
    }

    /**
     * Creates an instance of the IOException class using the specified exception.
     *
     * @param ex
     *            An Exception object that represents the exception used to create the IO exception.
     *
     * @return A java.io.IOException object that represents the created IO exception.
     */
    public static IOException initIOException(final Exception ex) {
        String message = null;
        if (ex != null && ex.getMessage() != null) {
            message = ex.getMessage() + " Please see the cause for further information.";
        }
        else {
            message = "Please see the cause for further information.";
        }

        final IOException retEx = new IOException(message, ex);
        return retEx;
    }

    /**
     * Returns a value that indicates whether the specified string is null or empty.
     *
     * @param value
     *            A String being examined for null or empty.
     *
     * @return true if the specified value is null or empty; otherwise, false
     */
    public static boolean isNullOrEmpty(final String value) {
        return value == null || value.length() == 0;
    }

    /**
     * Returns a value that indicates whether the specified string is null, empty, or whitespace.
     *
     * @param value
     *            A String being examined for null, empty, or whitespace.
     *
     * @return true if the specified value is null, empty, or whitespace; otherwise,
     *         false
     */
    public static boolean isNullOrEmptyOrWhitespace(final String value) {
        return value == null || value.trim().length() == 0;
    }

    /**
     * Parses a connection string and returns its values as a hash map of key/value pairs.
     *
     * @param parseString
     *            A String that represents the connection string to parse.
     *
     * @return A java.util.HashMap object that represents the hash map of the key / value pairs parsed from
     *         the connection string.
     */
    public static HashMap<String, String> parseAccountString(final String parseString) {

        // 1. split name value pairs by splitting on the ';' character
        final String[] valuePairs = parseString.split(";");
        final HashMap<String, String> retVals = new HashMap<String, String>();

        // 2. for each field value pair parse into appropriate map entries
        for (int m = 0; m < valuePairs.length; m++) {
            if (valuePairs[m].length() == 0) {
                continue;
            }
            final int equalDex = valuePairs[m].indexOf("=");
            if (equalDex < 1) {
                throw new IllegalArgumentException(SR.INVALID_CONNECTION_STRING);
            }

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

            // 2.1 add to map
            retVals.put(key, value);
        }

        return retVals;
    }

    /**
     * Returns a GMT date in the specified format
     *
     * @param value
     *            the string to parse
     * @return the GMT date, as a Date
     * @throws ParseException
     *             If the specified string is invalid
     */
    public static Date parseDateFromString(final String value, final String pattern, final TimeZone timeZone)
            throws ParseException {
        final DateFormat rfc1123Format = new SimpleDateFormat(pattern, Utility.LOCALE_US);
        rfc1123Format.setTimeZone(timeZone);
        return rfc1123Format.parse(value);
   }

    /**
     * Returns a GMT date for the specified string in the RFC1123 pattern.
     *
     * @param value
     *            A String that represents the string to parse.
     *
     * @return A Date object that represents the GMT date in the RFC1123 pattern.
     *
     * @throws ParseException
     *             If the specified string is invalid.
     */
    public static Date parseRFC1123DateFromStringInGMT(final String value) throws ParseException {
        final DateFormat format = new SimpleDateFormat(RFC1123_GMT_PATTERN, Utility.LOCALE_US);
        format.setTimeZone(GMT_ZONE);
        return format.parse(value);
    }

    /**
     * Performs safe decoding of the specified string, taking care to preserve each + character, rather
     * than replacing it with a space character.
     *
     * @param stringToDecode
     *            A String that represents the string to decode.
     *
     * @return A String that represents the decoded string.
     *
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static String safeDecode(final String stringToDecode) throws StorageException {
        if (stringToDecode == null) {
            return null;
        }

        if (stringToDecode.length() == 0) {
            return Constants.EMPTY_STRING;
        }

        try {
            if (stringToDecode.contains("+")) {
                final StringBuilder outBuilder = new StringBuilder();

                int startDex = 0;
                for (int m = 0; m < stringToDecode.length(); m++) {
                    if (stringToDecode.charAt(m) == '+') {
                        if (m > startDex) {
                            outBuilder.append(URLDecoder.decode(stringToDecode.substring(startDex, m),
                                    Constants.UTF8_CHARSET));
                        }

                        outBuilder.append("+");
                        startDex = m + 1;
                    }
                }

                if (startDex != stringToDecode.length()) {
                    outBuilder.append(URLDecoder.decode(stringToDecode.substring(startDex, stringToDecode.length()),
                            Constants.UTF8_CHARSET));
                }

                return outBuilder.toString();
            }
            else {
                return URLDecoder.decode(stringToDecode, Constants.UTF8_CHARSET);
            }
        }
        catch (final UnsupportedEncodingException e) {
            throw Utility.generateNewUnexpectedStorageException(e);
        }
    }

    /**
     * Performs safe encoding of the specified string, taking care to insert %20 for each space character,
     * instead of inserting the + character.
     *
     * @param stringToEncode
     *            A String that represents the string to encode.
     *
     * @return A String that represents the encoded string.
     *
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static String safeEncode(final String stringToEncode) throws StorageException {
        if (stringToEncode == null) {
            return null;
        }
        if (stringToEncode.length() == 0) {
            return Constants.EMPTY_STRING;
        }

        try {
            final String tString = URLEncoder.encode(stringToEncode, Constants.UTF8_CHARSET);

            if (stringToEncode.contains(" ")) {
                final StringBuilder outBuilder = new StringBuilder();

                int startDex = 0;
                for (int m = 0; m < stringToEncode.length(); m++) {
                    if (stringToEncode.charAt(m) == ' ') {
                        if (m > startDex) {
                            outBuilder.append(URLEncoder.encode(stringToEncode.substring(startDex, m),
                                    Constants.UTF8_CHARSET));
                        }

                        outBuilder.append("%20");
                        startDex = m + 1;
                    }
                }

                if (startDex != stringToEncode.length()) {
                    outBuilder.append(URLEncoder.encode(stringToEncode.substring(startDex, stringToEncode.length()),
                            Constants.UTF8_CHARSET));
                }

                return outBuilder.toString();
            }
            else {
                return tString;
            }

        }
        catch (final UnsupportedEncodingException e) {
            throw Utility.generateNewUnexpectedStorageException(e);
        }
    }

    /**
     * Determines the relative difference between the two specified URIs.
     *
     * @param baseURI
     *            A java.net.URI object that represents the base URI for which toUri will be
     *            made relative.
     * @param toUri
     *            A java.net.URI object that represents the URI to make relative to baseURI.
     *
     * @return A String that either represents the relative URI of toUri to
     *         baseURI, or the URI of toUri itself, depending on whether the hostname and
     *         scheme are identical for toUri and baseURI. If the hostname and scheme of
     *         baseURI and toUri are identical, this method returns an unencoded relative URI
     *         such that if appended to baseURI, it will yield toUri. If the hostname or
     *         scheme of baseURI and toUri are not identical, this method returns an unencoded
     *         full URI specified by toUri.
     *
     * @throws URISyntaxException
     *             If baseURI or toUri is invalid.
     */
    public static String safeRelativize(final URI baseURI, final URI toUri) throws URISyntaxException {
        // For compatibility followed
        // http://msdn.microsoft.com/en-us/library/system.uri.makerelativeuri.aspx

        // if host and scheme are not identical return from uri
        if (!baseURI.getHost().equals(toUri.getHost()) || !baseURI.getScheme().equals(toUri.getScheme())) {
            return toUri.toString();
        }

        final String basePath = baseURI.getPath();
        String toPath = toUri.getPath();

        int truncatePtr = 1;

        // Seek to first Difference
        // int maxLength = Math.min(basePath.length(), toPath.length());
        int m = 0;
        int ellipsesCount = 0;
        for (; m < basePath.length(); m++) {
            if (m >= toPath.length()) {
                if (basePath.charAt(m) == '/') {
                    ellipsesCount++;
                }
            }
            else {
                if (basePath.charAt(m) != toPath.charAt(m)) {
                    break;
                }
                else if (basePath.charAt(m) == '/') {
                    truncatePtr = m + 1;
                }
            }
        }

        // ../containername and ../containername/{path} should increment the truncatePtr
        // otherwise toPath will incorrectly begin with /containername
        if (m < toPath.length() && toPath.charAt(m) == '/') {
            // this is to handle the empty directory case with the '/' delimiter
            // for example, ../containername/ and ../containername// should not increment the truncatePtr
            if (!(toPath.charAt(m - 1) == '/' && basePath.charAt(m - 1) == '/')) {
                truncatePtr = m + 1;
            }
        }

        if (m == toPath.length()) {
            // No path difference, return query + fragment
            return new URI(null, null, null, toUri.getQuery(), toUri.getFragment()).toString();
        }
        else {
            toPath = toPath.substring(truncatePtr);
            final StringBuilder sb = new StringBuilder();
            while (ellipsesCount > 0) {
                sb.append("../");
                ellipsesCount--;
            }

            if (!Utility.isNullOrEmpty(toPath)) {
                sb.append(toPath);
            }

            if (!Utility.isNullOrEmpty(toUri.getQuery())) {
                sb.append("?");
                sb.append(toUri.getQuery());
            }
            if (!Utility.isNullOrEmpty(toUri.getFragment())) {
                sb.append("#");
                sb.append(toUri.getRawFragment());
            }

            return sb.toString();
        }
    }

    /**
     * Serializes the parsed StorageException. If an exception is encountered, returns empty string.
     *
     * @param ex
     *            The StorageException to serialize.
     * @param opContext
     *            The operation context which provides the logger.
     */
    public static void logHttpError(StorageException ex, OperationContext opContext) {
        if (Logger.shouldLog(opContext, Log.DEBUG)) {
            try {
                StringBuilder bld = new StringBuilder();
                bld.append("Error response received. ");

                bld.append("HttpStatusCode= ");
                bld.append(ex.getHttpStatusCode());

                bld.append(", HttpStatusMessage= ");
                bld.append(ex.getMessage());

                bld.append(", ErrorCode= ");
                bld.append(ex.getErrorCode());

                StorageExtendedErrorInformation extendedError = ex.getExtendedErrorInformation();
                if (extendedError != null) {
                    bld.append(", ExtendedErrorInformation= {ErrorMessage= ");
                    bld.append(extendedError.getErrorMessage());

                    HashMap<String, String[]> details = extendedError.getAdditionalDetails();
                    if (details != null) {
                        bld.append(", AdditionalDetails= { ");
                        for (Entry<String, String[]> detail : details.entrySet()) {
                            bld.append(detail.getKey());
                            bld.append("= ");

                            for (String value : detail.getValue()) {
                                bld.append(value);
                            }
                            bld.append(",");
                        }
                        bld.setCharAt(bld.length() - 1, '}');
                    }
                    bld.append("}");
                }

                Logger.debug(opContext, bld.toString());
            } catch (Exception e) {
                // Do nothing
            }
        }
    }

    /**
     * Logs the HttpURLConnection request. If an exception is encountered, logs nothing.
     *
     * @param conn
     *            The HttpURLConnection to serialize.
     * @param opContext
     *            The operation context which provides the logger.
     */
    public static void logHttpRequest(HttpURLConnection conn, OperationContext opContext) throws IOException {
        if (Logger.shouldLog(opContext, Log.VERBOSE)) {
            try {
                StringBuilder bld = new StringBuilder();

                bld.append(conn.getRequestMethod());
                bld.append(" ");
                bld.append(conn.getURL());
                bld.append("\n");

                // The Authorization header will not appear due to a security feature in HttpURLConnection
                for (Map.Entry<String, List<String>> header : conn.getRequestProperties().entrySet()) {
                    if (header.getKey() != null) {
                        bld.append(header.getKey());
                        bld.append(": ");
                    }

                    for (int i = 0; i < header.getValue().size(); i++) {
                        bld.append(header.getValue().get(i));
                        if (i < header.getValue().size() - 1) {
                            bld.append(",");
                        }
                    }
                    bld.append('\n');
                }

                Logger.verbose(opContext, bld.toString());
            } catch (Exception e) {
                // Do nothing
            }
        }
    }

    /**
     * Logs the HttpURLConnection response. If an exception is encountered, logs nothing.
     *
     * @param conn
     *            The HttpURLConnection to serialize.
     * @param opContext
     *            The operation context which provides the logger.
     */
    public static void logHttpResponse(HttpURLConnection conn, OperationContext opContext) throws IOException {
        if (Logger.shouldLog(opContext, Log.VERBOSE)) {
            try {
                StringBuilder bld = new StringBuilder();

                // This map's null key will contain the response code and message
                for (Map.Entry<String, List<String>> header : conn.getHeaderFields().entrySet()) {
                    if (header.getKey() != null) {
                        bld.append(header.getKey());
                        bld.append(": ");
                    }

                    for (int i = 0; i < header.getValue().size(); i++) {
                        bld.append(header.getValue().get(i));
                        if (i < header.getValue().size() - 1) {
                            bld.append(",");
                        }
                    }
                    bld.append('\n');
                }

                Logger.verbose(opContext, bld.toString());
            } catch (Exception e) {
                // Do nothing
            }
        }
    }

    /**
     * Trims the specified character from the end of a string.
     *
     * @param value
     *            A String that represents the string to trim.
     * @param trimChar
     *            The character to trim from the end of the string.
     *
     * @return The string with the specified character trimmed from the end.
     */
    protected static String trimEnd(final String value, final char trimChar) {
        int stopDex = value.length() - 1;
        while (stopDex > 0 && value.charAt(stopDex) == trimChar) {
            stopDex--;
        }

        return stopDex == value.length() - 1 ? value : value.substring(stopDex);
    }

    /**
     * Trims whitespace from the beginning of a string.
     *
     * @param value
     *            A String that represents the string to trim.
     *
     * @return The string with whitespace trimmed from the beginning.
     */
    public static String trimStart(final String value) {
        int spaceDex = 0;
        while (spaceDex < value.length() && value.charAt(spaceDex) == ' ') {
            spaceDex++;
        }

        return value.substring(spaceDex);
    }

    /**
     * Reads data from an input stream and writes it to an output stream, calculates the length of the data written, and
     * optionally calculates the MD5 hash for the data.
     *
     * @param sourceStream
     *            An InputStream object that represents the input stream to use as the source.
     * @param outStream
     *            An OutputStream object that represents the output stream to use as the destination.
     * @param writeLength
     *            The number of bytes to read from the stream.
     * @param rewindSourceStream
     *            true if the input stream should be rewound before it is read; otherwise,
     *            false
     * @param calculateMD5
     *            true if an MD5 hash will be calculated; otherwise, false.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     * @param options
     *            A {@link RequestOptions} object that specifies any additional options for the request. Namely, the
     *            maximum execution time.
     * @return A {@link StreamMd5AndLength} object that contains the output stream length, and optionally the MD5 hash.
     *
     * @throws IOException
     *             If an I/O error occurs.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static StreamMd5AndLength writeToOutputStream(final InputStream sourceStream, final OutputStream outStream,
            long writeLength, final boolean rewindSourceStream, final boolean calculateMD5, OperationContext opContext,
            final RequestOptions options) throws IOException, StorageException {
        return writeToOutputStream(sourceStream, outStream, writeLength, rewindSourceStream, calculateMD5, opContext,
                options, true);
    }

    /**
     * Reads data from an input stream and writes it to an output stream, calculates the length of the data written, and
     * optionally calculates the MD5 hash for the data.
     *
     * @param sourceStream
     *            An InputStream object that represents the input stream to use as the source.
     * @param outStream
     *            An OutputStream object that represents the output stream to use as the destination.
     * @param writeLength
     *            The number of bytes to read from the stream.
     * @param rewindSourceStream
     *            true if the input stream should be rewound before it is read; otherwise,
     *            false
     * @param calculateMD5
     *            true if an MD5 hash will be calculated; otherwise, false.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     * @param options
     *            A {@link RequestOptions} object that specifies any additional options for the request. Namely, the
     *            maximum execution time.
     * @return A {@link StreamMd5AndLength} object that contains the output stream length, and optionally the MD5 hash.
     *
     * @throws IOException
     *             If an I/O error occurs.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static StreamMd5AndLength writeToOutputStream(final InputStream sourceStream, final OutputStream outStream,
            long writeLength, final boolean rewindSourceStream, final boolean calculateMD5, OperationContext opContext,
            final RequestOptions options, final Boolean shouldFlush) throws IOException, StorageException {
        return writeToOutputStream(sourceStream, outStream, writeLength, rewindSourceStream, calculateMD5, opContext,
                options, null /*StorageRequest*/, null /* descriptor */);
    }

    /**
     * Reads data from an input stream and writes it to an output stream, calculates the length of the data written, and
     * optionally calculates the MD5 hash for the data.
     *
     * @param sourceStream
     *            An InputStream object that represents the input stream to use as the source.
     * @param outStream
     *            An OutputStream object that represents the output stream to use as the destination.
     * @param writeLength
     *            The number of bytes to read from the stream.
     * @param rewindSourceStream
     *            true if the input stream should be rewound before it is read; otherwise,
     *            false
     * @param calculateMD5
     *            true if an MD5 hash will be calculated; otherwise, false.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     * @param options
     *            A {@link RequestOptions} object that specifies any additional options for the request. Namely, the
     *            maximum execution time.
     * @param request
     *            Used by download resume to set currentRequestByteCount on the request. Otherwise, null is always used.
     * @param descriptor
     *                A {@Link StreamMd5AndLength} object to append to in the case of recovery action or null if this is not called
     *                from a recovery. This value needs to be passed for recovery in case part of the body has already been read,
     *                the recovery will attempt to download the remaining bytes but will do MD5 validation on the originally
     *                requested range size.
     * @return A {@link StreamMd5AndLength} object that contains the output stream length, and optionally the MD5 hash.
     *
     * @throws IOException
     *             If an I/O error occurs.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static StreamMd5AndLength writeToOutputStream(final InputStream sourceStream, final OutputStream outStream,
            long writeLength, final boolean rewindSourceStream, final boolean calculateMD5, OperationContext opContext,
            final RequestOptions options, StorageRequest<?, ?, Integer> request, StreamMd5AndLength descriptor)
            throws IOException, StorageException {
        if (rewindSourceStream && sourceStream.markSupported()) {
            sourceStream.reset();
            sourceStream.mark(Constants.MAX_MARK_LENGTH);
        }

        if (descriptor == null) {
            descriptor = new StreamMd5AndLength();
            if (calculateMD5) {
                try {
                    descriptor.setDigest(MessageDigest.getInstance("MD5"));
                }
                catch (final NoSuchAlgorithmException e) {
                    // This wont happen, throw fatal.
                    throw Utility.generateNewUnexpectedStorageException(e);
                }
            }
        }
        else {
            descriptor.setMd5(null);
        }

        if (writeLength < 0) {
            writeLength = Long.MAX_VALUE;
        }

        final byte[] retrievedBuff = new byte[Constants.BUFFER_COPY_LENGTH];
        int nextCopy = (int) Math.min(retrievedBuff.length, writeLength);
        int count = sourceStream.read(retrievedBuff, 0, nextCopy);

        while (nextCopy > 0 && count != -1) {

            // if maximum execution time would be exceeded
            if (Utility.validateMaxExecutionTimeout(options.getOperationExpiryTimeInMs())) {
                // throw an exception
                TimeoutException timeoutException = new TimeoutException(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION);
                throw Utility.initIOException(timeoutException);
            }

            if (outStream != null) {
                outStream.write(retrievedBuff, 0, count);
            }

            if (calculateMD5) {
                descriptor.getDigest().update(retrievedBuff, 0, count);
            }

            descriptor.setLength(descriptor.getLength() + count);
            descriptor.setCurrentOperationByteCount(descriptor.getCurrentOperationByteCount() + count);

            if (request != null) {
                request.setCurrentRequestByteCount(request.getCurrentRequestByteCount() + count);
                request.setCurrentDescriptor(descriptor);
            }

            nextCopy = (int) Math.min(retrievedBuff.length, writeLength - descriptor.getLength());
            count = sourceStream.read(retrievedBuff, 0, nextCopy);
        }

        if (outStream != null) {
            outStream.flush();
        }

        return descriptor;
    }

    /**
     * Private Default Constructor.
     */
    private Utility() {
        // No op
    }

    public static void checkNullaryCtor(Class<?> clazzType) {
        Constructor<?> ctor = null;
        try {
            ctor = clazzType.getDeclaredConstructor((Class<?>[]) null);
        }
        catch (Exception e) {
            throw new IllegalArgumentException(SR.MISSING_NULLARY_CONSTRUCTOR);
        }

        if (ctor == null) {
            throw new IllegalArgumentException(SR.MISSING_NULLARY_CONSTRUCTOR);
        }
    }

    /**
     * Given a String representing a date in a form of the ISO8601 pattern, generates a Date representing it
     * with up to millisecond precision.
     *
     * @param dateString
     *              the String to be interpreted as a Date
     *
     * @return the corresponding Date object
     */
    public static Date parseDate(String dateString) {
        String pattern = MAX_PRECISION_PATTERN;
        switch(dateString.length()) {
            case 28: // "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'"-> [2012-01-04T23:21:59.1234567Z] length = 28
            case 27: // "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"-> [2012-01-04T23:21:59.123456Z] length = 27
            case 26: // "yyyy-MM-dd'T'HH:mm:ss.SSSSS'Z'"-> [2012-01-04T23:21:59.12345Z] length = 26
            case 25: // "yyyy-MM-dd'T'HH:mm:ss.SSSS'Z'"-> [2012-01-04T23:21:59.1234Z] length = 25
            case 24: // "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"-> [2012-01-04T23:21:59.123Z] length = 24
                dateString = dateString.substring(0, MAX_PRECISION_DATESTRING_LENGTH);
                break;
            case 23: // "yyyy-MM-dd'T'HH:mm:ss.SS'Z'"-> [2012-01-04T23:21:59.12Z] length = 23
                // SS is assumed to be milliseconds, so a trailing 0 is necessary
                dateString = dateString.replace("Z", "0");
                break;
            case 22: // "yyyy-MM-dd'T'HH:mm:ss.S'Z'"-> [2012-01-04T23:21:59.1Z] length = 22
                // S is assumed to be milliseconds, so trailing 0's are necessary
                dateString = dateString.replace("Z", "00");
                break;
            case 20: // "yyyy-MM-dd'T'HH:mm:ss'Z'"-> [2012-01-04T23:21:59Z] length = 20
                pattern = Utility.ISO8601_PATTERN;
                break;
            case 17: // "yyyy-MM-dd'T'HH:mm'Z'"-> [2012-01-04T23:21Z] length = 17
                pattern = Utility.ISO8601_PATTERN_NO_SECONDS;
                break;
            default:
                throw new IllegalArgumentException(String.format(SR.INVALID_DATE_STRING, dateString));
        }

        final DateFormat format = new SimpleDateFormat(pattern, Utility.LOCALE_US);
        format.setTimeZone(UTC_ZONE);
        try {
            return format.parse(dateString);
        }
        catch (final ParseException e) {
            throw new IllegalArgumentException(String.format(SR.INVALID_DATE_STRING, dateString), e);
        }
    }

    /**
     * Given a String representing a date in a form of the ISO8601 pattern, generates a Date representing it
     * with up to millisecond precision. Use {@link #parseDate(String)} instead unless
     * dateBackwardCompatibility is needed.
     *

     * See here for more details.
     *
     * @param dateString
     *              the String to be interpreted as a Date
     * @param dateBackwardCompatibility
     *              true to correct Date values that may have been written
     *              using versions of this library prior to 0.4.0; otherwise, false
     *
     * @return the corresponding Date object
     */
    public static Date parseDate(String dateString, boolean dateBackwardCompatibility) {
        if (!dateBackwardCompatibility) {
            return parseDate(dateString);
        }

        final int beginMilliIndex = 20;     // Length of "yyyy-MM-ddTHH:mm:ss."
        final int endTenthMilliIndex = 24;  // Length of "yyyy-MM-ddTHH:mm:ss.SSSS"

        // Check whether the millisecond and tenth of a millisecond digits are all 0.
        if (dateString.length() > endTenthMilliIndex &&
                "0000".equals(dateString.substring(beginMilliIndex, endTenthMilliIndex))) {
            // Remove the millisecond and tenth of a millisecond digits.
            // Treat the final three digits (ticks) as milliseconds.
            dateString = dateString.substring(0, beginMilliIndex) + dateString.substring(endTenthMilliIndex);
        }

        return parseDate(dateString);
    }

    /**
     * Determines which location can the listing command target by looking at the
     * continuation token.
     *
     * @param token
     *            Continuation token
     * @return
     *         Location mode
     */
    public static RequestLocationMode getListingLocationMode(ResultContinuation token) {
        if ((token != null) && token.getTargetLocation() != null) {
            switch (token.getTargetLocation()) {
                case PRIMARY:
                    return RequestLocationMode.PRIMARY_ONLY;

                case SECONDARY:
                    return RequestLocationMode.SECONDARY_ONLY;

                default:
                    throw new IllegalArgumentException(String.format(SR.ARGUMENT_OUT_OF_RANGE_ERROR, "token",
                            token.getTargetLocation()));
            }
        }

        return RequestLocationMode.PRIMARY_OR_SECONDARY;
    }

    /**
     * Writes an XML element to the XmlSerializer without a namespace.
     *
     * @param serializer
     *            the XmlSerializer to write to
     * @param tag
     *            the tag to use for the XML element
     * @param text
     *            the text to write inside the XML tags
     * @throws IOException
     *             if there is an error writing the output.
     * @throws IllegalStateException
     *             if there is an error writing the output.
     * @throws IllegalArgumentException
     *             if there is an error writing the output.
     */
    public static void serializeElement(XmlSerializer serializer, String tag, String text) throws IllegalArgumentException,
            IllegalStateException, IOException {
        serializer.startTag(Constants.EMPTY_STRING, tag);
        serializer.text(text);
        serializer.endTag(Constants.EMPTY_STRING, tag);
    }
}


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

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

/**
 * Represents a set of options that may be specified on a queue request.
 */
public final class QueueRequestOptions extends RequestOptions {
    /**
     * Initializes a new instance of the QueueRequestOptions class.
     */
    public QueueRequestOptions() {
        // no op
    }

    /**
     * Initializes a new instance of the QueueRequestOptions class as a copy of
     * another QueueRequestOptions instance.
     *
     * @param other
     *            The {@link QueueRequestOptions} object to copy the values
     *            from.
     */
    public QueueRequestOptions(final QueueRequestOptions other) {
        super(other);
    }

    /**
     * Populates the default timeout and retry policy from client if they are not set.
     *
     * @param options
     *            The input options to copy from when applying defaults
     * @param client
     *            The {@link CloudQueueClient} service client to populate the
     *            default values from.
     */
    protected static final QueueRequestOptions populateAndApplyDefaults(QueueRequestOptions options, final CloudQueueClient client) {
        QueueRequestOptions modifiedOptions = new QueueRequestOptions(options);
        RequestOptions.populateRequestOptions(modifiedOptions, client.getDefaultRequestOptions(), true /* setStartTime */);
        QueueRequestOptions.applyDefaults(modifiedOptions);
        return modifiedOptions;
    }

    /**
     * Applies defaults to the options passed in.
     *
     * @param modifiedOptions
     *          The options to apply defaults to.
     */
    protected static void applyDefaults(QueueRequestOptions modifiedOptions) {
        Utility.assertNotNull("modifiedOptions", modifiedOptions);
        RequestOptions.applyBaseDefaultsInternal(modifiedOptions);
    }
}


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

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

/**
 * Represents a shared access policy, which specifies the start time, expiry time, and permissions for a shared access
 * signature.
 */
public final class SharedAccessTablePolicy extends SharedAccessPolicy {
    /**
     * The permissions for a shared access signature associated with this shared access policy.
     */
    private EnumSet<SharedAccessTablePermissions> permissions;

    /**
     * Gets the permissions for a shared access signature associated with this shared access policy.
     *
     * @return A java.util.EnumSet object that contains {@link SharedAccessTablePermissions} values that
     *         represents the set of shared access permissions.
     */
    public EnumSet<SharedAccessTablePermissions> getPermissions() {
        return this.permissions;
    }

    /**
     * Sets the permissions for a shared access signature associated with this shared access policy.
     *
     * @param permissions
     *            The permissions, represented by a java.util.EnumSet object that contains
     *            {@link SharedAccessTablePermissions} values, to set for the shared access signature.
     */
    public void setPermissions(final EnumSet<SharedAccessTablePermissions> permissions) {
        this.permissions = permissions;
    }

    /**
     * Converts this policy's permissions to a string.
     *
     * @return A String that represents the shared access permissions in the "raud" format, which is
     *         described at {@link SharedAccessTablePolicy#setPermissionsFromString(String)}.
     */
    @Override
    public String permissionsToString() {
        if (this.permissions == null) {
            return Constants.EMPTY_STRING;
        }

        // The service supports a fixed order => raud
        final StringBuilder builder = new StringBuilder();

        if (this.permissions.contains(SharedAccessTablePermissions.QUERY)) {
            builder.append("r");
        }

        if (this.permissions.contains(SharedAccessTablePermissions.ADD)) {
            builder.append("a");
        }

        if (this.permissions.contains(SharedAccessTablePermissions.UPDATE)) {
            builder.append("u");
        }

        if (this.permissions.contains(SharedAccessTablePermissions.DELETE)) {
            builder.append("d");
        }

        return builder.toString();
    }

    /**
     * Sets shared access permissions using the specified permissions string.
     *
     * @param value
     *            A String that represents the shared access permissions. The string must contain one or
     *            more of the following values. Note that they must be lower case, and the order that they are specified
     *            must be in the order of "raud".
     *

     *
r: Query access.

     *
a: Add access.

     *
u: Update access.

     *
d: Delete access.

     *

     */
    @Override
    public void setPermissionsFromString(final String value) {
        final char[] chars = value.toCharArray();
        final EnumSet<SharedAccessTablePermissions> retSet = EnumSet.noneOf(SharedAccessTablePermissions.class);

        for (final char c : chars) {
            switch (c) {
                case 'r':
                    retSet.add(SharedAccessTablePermissions.QUERY);
                    break;
                case 'a':
                    retSet.add(SharedAccessTablePermissions.ADD);
                    break;
                case 'u':
                    retSet.add(SharedAccessTablePermissions.UPDATE);
                    break;
                case 'd':
                    retSet.add(SharedAccessTablePermissions.DELETE);
                    break;
                default:
                    throw new IllegalArgumentException(String.format(SR.ENUM_COULD_NOT_BE_PARSED, "Permissions", value));
            }
        }

        this.permissions = retSet;
    }
}


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

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.UUID;

import android.text.Html;

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

/**
 * RESERVED FOR INTERNAL USE. Reads LogRecord information from an InputStream.
 */
class LogRecordStreamReader extends InputStreamReader {

    /**
     * Holds the field delimiter character.
     */
    public static final char FIELD_DELIMITER = ';';

    /**
     * Holds the record delimiter character.
     */
    public static final char RECORD_DELIMITER = '\n';

    /**
     * Holds the quote character.
     */
    public static final char QUOTE_CHAR = '"';

    /**
     * Flag that indicates whether this is the start of a record.
     */
    private boolean isFirstFieldInRecord;

    /**
     * Peeked character, if it has been pulled already.
     */
    private Integer peekedCharacter = null;

    /**
     * Constructs a new LogRecordStreamReader to read the stream given.
     *
     * @param stream
     */
    public LogRecordStreamReader(InputStream stream) {
        super(stream);

        this.isFirstFieldInRecord = true;
    }

    /**
     * Checks if another field exists in the record.
     *
     * @return
     *         true if another field exists, false otherwise.
     * @throws IOException
     */
    public boolean hasMoreFieldsInRecord() throws IOException {
        return this.tryPeekDelimiter(LogRecordStreamReader.FIELD_DELIMITER);
    }

    /**
     * Checks to see if the end of the stream has been reached.
     *
     * @return
     *         true if at the end of the stream, false otherwise.
     * @throws IOException
     */
    public boolean isEndOfFile() throws IOException {
        return (this.peek() == -1);
    }

    /**
     * Read a String from the stream.
     *
     * @return
     *         the String read.
     * @throws IOException
     */
    public String readString() throws IOException {
        String temp = this.readField(false /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return temp;
        }
    }

    /**
     * Read a quoted String from the stream.
     *
     * @return
     *         the String read.
     * @throws IOException
     */
    public String readQuotedString() throws IOException {
        String temp = this.readField(true /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return temp;
        }
    }

    /**
     * Read a Boolean from the stream.
     *
     * @return
     *         the Boolean read.
     * @throws IOException
     */
    public Boolean readBoolean() throws IOException {
        String temp = this.readField(false /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return Boolean.parseBoolean(temp);
        }
    }

    /**
     * Read a Date from the stream.
     *
     * @param format
     *            the format in which the date is stored, for parsing purposes.
     * @return
     *         the Date read.
     * @throws IOException
     * @throws ParseException
     */
    public Date readDate(DateFormat format) throws IOException, ParseException {
        String temp = this.readField(false /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return format.parse(temp);
        }
    }

    /**
     * Read a Double from the stream.
     *
     * @return
     *         the Double read.
     * @throws IOException
     */
    public Double readDouble() throws IOException {
        String temp = this.readField(false /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return Double.parseDouble(temp);
        }
    }

    /**
     * Read a UUID from the stream.
     *
     * @return
     *         the UUID read.
     * @throws IOException
     */
    public UUID readUuid() throws IOException {
        String temp = this.readField(false /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return UUID.fromString(temp);
        }
    }

    /**
     * Read an Integer from the stream.
     *
     * @return
     *         the Integer read.
     * @throws IOException
     */
    public Integer readInteger() throws IOException {
        String temp = this.readField(false /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return Integer.parseInt(temp);
        }
    }

    /**
     * Read a Long from the stream.
     *
     * @return
     *         the Long read.
     * @throws IOException
     */
    public Long readLong() throws IOException {
        String temp = this.readField(false /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return Long.parseLong(temp);
        }
    }

    /**
     * Read a URI from the stream.
     *
     * @return
     *         the URI read.
     * @throws URISyntaxException
     * @throws IOException
     */
    public URI readUri() throws URISyntaxException, IOException {
        String temp = this.readField(true /* isQuotedString */);

        if (Utility.isNullOrEmpty(temp)) {
            return null;
        }
        else {
            return new URI(Html.fromHtml(temp).toString());
        }
    }

    /**
     * Ends the current record by reading the record delimiter and adjusting internal state.
     *
     * @throws IOException
     */
    public void endCurrentRecord() throws IOException {
        this.readDelimiter(LogRecordStreamReader.RECORD_DELIMITER);
        this.isFirstFieldInRecord = true;
    }

    /**
     * Read a delimiter from the stream.
     *
     * @param delimiter
     *            the delimiter to read.
     * @throws IOException
     */
    private void readDelimiter(char delimiter) throws IOException {
        if (this.isEndOfFile()) {
            throw new EOFException(SR.LOG_STREAM_END_ERROR);
        }
        else {
            int read = this.read();
            if (read == -1 || (char) read != delimiter) {
                throw new IllegalStateException(SR.LOG_STREAM_DELIMITER_ERROR);
            }
        }
    }

    /**
     * Checks to see if the next character is the delimiter expected.
     *
     * @param delimiter
     *            the delimiter to try to peek.
     * @return
     * @throws IOException
     */
    private boolean tryPeekDelimiter(char delimiter) throws IOException {
        if (this.isEndOfFile()) {
            throw new EOFException(SR.LOG_STREAM_END_ERROR);
        }
        else {
            if ((char) this.peek() != delimiter) {
                return false;
            }
            else {
                return true;
            }
        }
    }

    /**
     * Read a field from the stream.
     *
     * @param isQuotedString
     *            whether the field is encased in quotes and escaped.
     * @return
     *         the field read.
     * @throws IOException
     */
    private String readField(boolean isQuotedString) throws IOException {
        if (!this.isFirstFieldInRecord) {
            this.readDelimiter(LogRecordStreamReader.FIELD_DELIMITER);
        }
        else {
            this.isFirstFieldInRecord = false;
        }

        // Read a field, handling field/record delimiters in quotes and not counting them,
        // and also check that there are no record delimiters since they are only expected
        // outside of a field.
        // Note: We only need to handle strings that are quoted once from the beginning,
        // (e.g. "mystring"). We do not need to handle nested quotes or anything because
        // we control the string format.
        StringBuilder fieldBuilder = new StringBuilder();
        boolean hasSeenQuoteForQuotedString = false;
        boolean isExpectingDelimiterForNextCharacterForQuotedString = false;
        while (true) {
            // If EOF when we haven't read the delimiter; unexpected.
            if (this.isEndOfFile()) {
                throw new EOFException(SR.LOG_STREAM_END_ERROR);
            }

            // Since we just checked isEndOfFile above, we know this is a char.
            char c = (char) this.peek();

            // If we hit a delimiter that is not quoted or we hit the delimiter for
            // a quoted string or we hit the empty value string and hit a delimiter,
            // then we have finished reading the field.
            // Note: The empty value string is the only string that we don't require
            // quotes for for a quoted string.
            if ((!isQuotedString || isExpectingDelimiterForNextCharacterForQuotedString || fieldBuilder.length() == 0)
                    && (c == LogRecordStreamReader.FIELD_DELIMITER || c == LogRecordStreamReader.RECORD_DELIMITER)) {
                // The delimiter character was peeked but not read -- they'll be consumed
                // on either the next call to readField or to EndCurrentRecord.
                break;
            }

            if (isExpectingDelimiterForNextCharacterForQuotedString) {
                // We finished reading a quoted string but found no delimiter following it.
                throw new IllegalStateException(SR.LOG_STREAM_QUOTE_ERROR);
            }

            // The character was not a delimiter, so consume and add to builder.
            this.read();
            fieldBuilder.append(c);

            // We need to handle quotes specially since quoted delimiters
            // do not count since they are considered to be part of the
            // quoted string and not actually a delimiter.
            // Note: We use a specific quote character since we control the format
            // and we only allow non-encoded quote characters at the beginning/end
            // of the string.
            if (c == LogRecordStreamReader.QUOTE_CHAR) {
                if (!isQuotedString) {
                    // Non-encoded quote character only allowed for quoted strings.
                    throw new IllegalStateException(SR.LOG_STREAM_QUOTE_ERROR);
                }
                else if (fieldBuilder.length() == 1) {
                    // Opening quote for a quoted string.
                    hasSeenQuoteForQuotedString = true;
                }
                else if (hasSeenQuoteForQuotedString) {
                    // Closing quote for a quoted string.
                    isExpectingDelimiterForNextCharacterForQuotedString = true;
                }
                else {
                    // Unexpected non-encoded quote.
                    throw new IllegalStateException(SR.LOG_STREAM_QUOTE_ERROR);
                }
            }
        }

        String field;

        // Note: For quoted strings we remove the quotes.
        // We do not do this for the empty value string since it represents empty
        // and we don't write that out in quotes even for quoted strings.
        if (isQuotedString && fieldBuilder.length() != 0) {
            field = fieldBuilder.substring(1, fieldBuilder.length() - 1);
        }
        else {
            field = fieldBuilder.toString();
        }

        return field;
    }

    @Override
    public int read() throws IOException {
        // Before calling the super, check to see if a char has been peeked.
        // Short circuit, reset it, and return it if so.
        if (this.peekedCharacter != null) {
            int temp = this.peekedCharacter;
            this.peekedCharacter = null;
            return temp;
        }
        else {
            return super.read();
        }
    }

    /**
     * Peek a character from the stream. This character is not consumed until read() is called.
     *
     * @return
     *         the character peeked, or -1 if none exist.
     * @throws IOException
     */
    protected int peek() throws IOException {
        if (this.peekedCharacter != null) {
            // If we already have peeked, just return that.
            return this.peekedCharacter;
        }
        else {
            this.peekedCharacter = super.read();
            return this.peekedCharacter;
        }
    }
}


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

import java.net.HttpURLConnection;

/**
 * Represents a retry policy that performs a specified number of retries, using a specified fixed time interval between
 * retries.
 *
 * This class extends the {@link RetryPolicy} class and implements the {@link RetryPolicyFactory} interface.
 */
public final class RetryLinearRetry extends RetryPolicy implements RetryPolicyFactory {

    /**
     * Creates an instance of the RetryLinearRetry class.
     */
    public RetryLinearRetry() {
        this(RetryPolicy.DEFAULT_CLIENT_BACKOFF, RetryPolicy.DEFAULT_CLIENT_RETRY_COUNT);
    }

    /**
     * Creates an instance of the RetryLinearRetry class using the specified delta backoff and maximum
     * retry attempts.
     *
     * @param deltaBackoff
     *            The backoff interval, in milliseconds, between retries.
     * @param maxAttempts
     *            The maximum number of retry attempts.
     */
    public RetryLinearRetry(final int deltaBackoff, final int maxAttempts) {
        super(deltaBackoff, maxAttempts);
    }

    /**
     * Generates a new retry policy for the current request attempt.
     *
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     *
     * @return A {@link RetryPolicy} object that represents the retry policy for the current request attempt.
     */
    @Override
    public RetryPolicy createInstance(final OperationContext opContext) {
        return new RetryLinearRetry(this.deltaBackoffIntervalInMs, this.maximumAttempts);
    }

    /**
     * Determines whether the operation should be retried and specifies the interval until the next retry.
     *
     * @param retryContext
     *            A {@link RetryContext} object that indicates the number of retries, last request's results, whether
     *            the next retry should happen in the primary or secondary location, and specifies the location mode.
     * @param operationContext
     *            An {@link OperationContext} object for tracking the current operation.
     * @return
     *         A {@link RetryInfo} object that indicates whether the next retry will happen in the primary or secondary
     *         location, and specifies the location mode. If null, the operation will not be retried.
     */
    @Override
    public RetryInfo evaluate(RetryContext retryContext, OperationContext operationContext) {

        boolean secondaryNotFound = this.evaluateLastAttemptAndSecondaryNotFound(retryContext);

        if (retryContext.getCurrentRetryCount() < this.maximumAttempts) {

            // If this method is called after a successful response, it means
            // we failed during the response body download. So, we should not
            // check for success codes here.
            int statusCode = retryContext.getLastRequestResult().getStatusCode();
            if ((!secondaryNotFound && statusCode >= 300 && statusCode < 500 && statusCode != 408)
                    || statusCode == HttpURLConnection.HTTP_NOT_IMPLEMENTED
                    || statusCode == HttpURLConnection.HTTP_VERSION) {
                return null;
            }

            final long retryInterval = Math.max(
                    Math.min(this.deltaBackoffIntervalInMs, RetryPolicy.DEFAULT_MAX_BACKOFF),
                    RetryPolicy.DEFAULT_MIN_BACKOFF);

            return this.evaluateRetryInfo(retryContext, secondaryNotFound, retryInterval);
        }

        return null;
    }
}


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

import java.net.HttpURLConnection;
import java.util.Random;

/**
 * Represents a retry policy that performs a specified number of retries, using a randomized exponential backoff scheme
 * to determine the interval between retries.
 *
 * This class extends the {@link com.microsoft.azure.storage.RetryPolicy} class and implements the
 * {@link com.microsoft.azure.storage.RetryPolicyFactory} interface.
 */
public final class RetryExponentialRetry extends RetryPolicy implements RetryPolicyFactory {

    /**
     * Holds the random number generator used to calculate randomized backoff intervals.
     */
    private final Random randRef = new Random();

    /**
     * Holds the actual maximum backoff interval to enforce.
     */
    private int resolvedMaxBackoff = RetryPolicy.DEFAULT_MAX_BACKOFF;

    /**
     * Holds the actual minimum backoff interval to enforce.
     */
    private int resolvedMinBackoff = RetryPolicy.DEFAULT_MIN_BACKOFF;

    /**
     * Creates an instance of the RetryExponentialRetry class.
     */
    public RetryExponentialRetry() {
        this(RetryPolicy.DEFAULT_CLIENT_BACKOFF, RetryPolicy.DEFAULT_CLIENT_RETRY_COUNT);
    }

    /**
     * Creates an instance of the RetryExponentialRetry class using the specified delta backoff and maximum
     * retry attempts.
     *
     * @param deltaBackoff
     *            The backoff interval, in milliseconds, between retries.
     * @param maxAttempts
     *            The maximum number of retry attempts.
     */
    public RetryExponentialRetry(final int deltaBackoff, final int maxAttempts) {
        super(deltaBackoff, maxAttempts);
    }

    /**
     * Creates an instance of the RetryExponentialRetry class using the specified minimum, maximum, and
     * delta backoff amounts, and maximum number of retry attempts.
     *
     * @param minBackoff
     *            The minimum backoff interval, in milliseconds, between retries.
     * @param deltaBackoff
     *            The backoff interval, in milliseconds, between retries.
     * @param maxBackOff
     *            The maximum backoff interval, in milliseconds, between retries.
     * @param maxAttempts
     *            The maximum retry attempts, in milliseconds, between retries.
     */
    public RetryExponentialRetry(final int minBackoff, final int deltaBackoff, final int maxBackOff,
            final int maxAttempts) {
        super(deltaBackoff, maxAttempts);
        this.resolvedMinBackoff = minBackoff;
        this.resolvedMaxBackoff = maxBackOff;
    }

    /**
     * Generates a new retry policy for the current request attempt.
     *
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     *
     * @return A {@link RetryPolicy} object that represents the retry policy for the current request attempt.
     */
    @Override
    public RetryPolicy createInstance(final OperationContext opContext) {
        return new RetryExponentialRetry(this.resolvedMinBackoff, this.deltaBackoffIntervalInMs,
                this.resolvedMaxBackoff, this.maximumAttempts);
    }

    /**
     * Determines whether the operation should be retried and specifies the interval until the next retry.
     *
     * @param retryContext
     *            A {@link RetryContext} object that indicates the number of retries, last request's results, whether
     *            the next retry should happen in the primary or secondary location, and specifies the location mode.
     * @param operationContext
     *            An {@link OperationContext} object for tracking the current operation.
     * @return
     *         A {@link RetryInfo} object that indicates whether the next retry will happen in the primary or secondary
     *         location, and specifies the location mode. If null, the operation will not be retried.
     */
    @Override
    public RetryInfo evaluate(RetryContext retryContext, OperationContext operationContext) {

        boolean secondaryNotFound = this.evaluateLastAttemptAndSecondaryNotFound(retryContext);

        if (retryContext.getCurrentRetryCount() < this.maximumAttempts) {

            // If this method is called after a successful response, it means
            // we failed during the response body download. So, we should not
            // check for success codes here.
            int statusCode = retryContext.getLastRequestResult().getStatusCode();
            if ((!secondaryNotFound && statusCode >= 300 && statusCode < 500 && statusCode != 408)
                    || statusCode == HttpURLConnection.HTTP_NOT_IMPLEMENTED
                    || statusCode == HttpURLConnection.HTTP_VERSION) {
                return null;
            }

            // Calculate backoff Interval between 80% and 120% of the desired
            // backoff, multiply by 2^n -1 for exponential
            double incrementDelta = (Math.pow(2, retryContext.getCurrentRetryCount()) - 1);
            final int boundedRandDelta = (int) (this.deltaBackoffIntervalInMs * 0.8)
                    + this.randRef.nextInt((int) (this.deltaBackoffIntervalInMs * 1.2)
                            - (int) (this.deltaBackoffIntervalInMs * 0.8));
            incrementDelta *= boundedRandDelta;

            final long retryInterval = (int) Math.round(Math.min(this.resolvedMinBackoff + incrementDelta,
                    this.resolvedMaxBackoff));

            return this.evaluateRetryInfo(retryContext, secondaryNotFound, retryInterval);
        }

        return null;
    }
}


/**
 * 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.util.Iterator;
import java.util.NoSuchElementException;

import com.microsoft.azure.storage.DoesServiceRequest;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.ResultSegment;
import com.microsoft.azure.storage.RetryPolicyFactory;
import com.microsoft.azure.storage.StorageException;

/**
 * RESERVED FOR INTERNAL USE. Provides a lazy iterator which will retrieve the next segment of a result as the iterator
 * is consumed
 *
 * @param <CLIENT_TYPE>
 *            The service client type
 * @param <PARENT_TYPE>
 *            The type of the parent object, i.e. CloudBlobClient for ListContainers etc.
 * @param <ENTITY_TYPE>
 *            The type of the objects the resulting iterable objects
 */
public final class LazySegmentedIterator<CLIENT_TYPE, PARENT_TYPE, ENTITY_TYPE> implements Iterator<ENTITY_TYPE> {

    /**
     * Holds the current segment of results.
     */
    private ResultSegment<ENTITY_TYPE> currentSegment;

    /**
     * Holds the iterator for the current Segment.
     */
    private Iterator<ENTITY_TYPE> currentSegmentIterator;

    /**
     * Holds the service client associated with the operations.
     */
    private final CLIENT_TYPE client;

    /**
     * Holds a reference to the parent object, i.e. CloudBlobContainer for list blobs.
     */
    private final PARENT_TYPE parentObject;

    /**
     * Holds the reference to the RetryPolicyFactory object.
     */
    private final RetryPolicyFactory policyFactory;

    /**
     * Holds the SegmentedStorageRequest which is used to retrieve the next segment of results.
     */
    private final StorageRequest<CLIENT_TYPE, PARENT_TYPE, ResultSegment<ENTITY_TYPE>> segmentGenerator;

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

    /**
     * Initializes the LazySegmentedIterator.
     *
     * @param segmentGenerator
     *            a SegmentedStorageRequest to execute in order to retrieve the next segment of the result.
     * @param client
     *            the service client associated with the request
     * @param parent
     *            the parent object
     * @param policyFactory
     *            the factory used to generate a new retry policy instance
     * @param opContext
     *            an object used to track the execution of the operation
     */
    public LazySegmentedIterator(
            final StorageRequest<CLIENT_TYPE, PARENT_TYPE, ResultSegment<ENTITY_TYPE>> segmentGenerator,
            final CLIENT_TYPE client, final PARENT_TYPE parent, final RetryPolicyFactory policyFactory,
            final OperationContext opContext) {
        this.segmentGenerator = segmentGenerator;
        this.parentObject = parent;
        this.opContext = opContext;
        this.policyFactory = policyFactory;
        this.client = client;
    }

    /**
     * Indicates if the iterator has another element.
     */
    @Override
    @DoesServiceRequest
    public boolean hasNext() {
        while (this.currentSegment == null
                || (!this.currentSegmentIterator.hasNext() && this.currentSegment != null && this.currentSegment
                        .getHasMoreResults())) {
            try {
                this.currentSegment = ExecutionEngine.executeWithRetry(this.client, this.parentObject,
                        this.segmentGenerator, this.policyFactory, this.opContext);
            }
            catch (final StorageException e) {
                final NoSuchElementException ex = new NoSuchElementException(SR.ENUMERATION_ERROR);
                ex.initCause(e);
                throw ex;
            }
            this.currentSegmentIterator = this.currentSegment.getResults().iterator();

            if (!this.currentSegmentIterator.hasNext() && !this.currentSegment.getHasMoreResults()) {
                return false;
            }
        }

        return this.currentSegmentIterator.hasNext();
    }

    /**
     * Returns the next element.
     */
    @Override
    public ENTITY_TYPE next() {
        if (this.hasNext()) {
            return this.currentSegmentIterator.next();
        } else {
            throw new NoSuchElementException();
        }
    }

    /**
     * Removes an element, not supported
     */
    @Override
    public void remove() {
        // read only, no-op
        throw new UnsupportedOperationException();
    }
}


/**
 * 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 com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;

/***
 * RESERVED FOR INTERNAL USE. A class to hold utility methods for parsing OData payloads
 */
public final class JsonUtilities {
    /***
     * Reserved for internal use. Asserts that the current name of the parser equals the expected value
     *
     * @param parser
     *            The {@link JsonParser} whose current token to check.
     * @param expectedValue
     *            The expected current name of the parser's current token.
     */
    public static void assertIsExpectedFieldName(final JsonParser parser, final String expectedValue)
            throws JsonParseException, IOException {
        final String actualValue = parser.getCurrentName();
        if (expectedValue == null) {
            if (actualValue != null) {
                throw new JsonParseException(String.format(SR.UNEXPECTED_FIELD_NAME, expectedValue, actualValue),
                        parser.getCurrentLocation());
            }
        }
        else {
            if (!expectedValue.equals(actualValue)) {
                throw new JsonParseException(String.format(SR.UNEXPECTED_FIELD_NAME, expectedValue, actualValue),
                        parser.getCurrentLocation());
            }
        }
    }

    /***
     * Reserved for internal use. Asserts that the current token of the parser is a field name.
     *
     * @param parser
     *            The {@link JsonParser} whose current token to check.
     */
    public static void assertIsFieldNameJsonToken(final JsonParser parser) throws JsonParseException {
        if (!(parser.getCurrentToken() == JsonToken.FIELD_NAME)) {
            throw new JsonParseException(SR.EXPECTED_A_FIELD_NAME, parser.getCurrentLocation());
        }
    }

    /***
     * Reserved for internal use. Asserts that the current token of the parser is the start of an object.
     *
     * @param parser
     *            The {@link JsonParser} whose current token to check.
     */
    public static void assertIsStartObjectJsonToken(final JsonParser parser) throws JsonParseException {
        if (!(parser.getCurrentToken() == JsonToken.START_OBJECT)) {
            throw new JsonParseException(SR.EXPECTED_START_OBJECT, parser.getCurrentLocation());
        }
    }

    /***
     * Reserved for internal use. Asserts that the current token of the parser is the end of an object.
     *
     * @param parser
     *            The {@link JsonParser} whose current token to check.
     */
    public static void assertIsEndObjectJsonToken(final JsonParser parser) throws JsonParseException {
        if (!(parser.getCurrentToken() == JsonToken.END_OBJECT)) {
            throw new JsonParseException(SR.EXPECTED_END_OBJECT, parser.getCurrentLocation());
        }
    }

    /***
     * Reserved for internal use. Asserts that the token type of the parser is the start of an array.
     *
     * @param parser
     *            The {@link JsonParser} whose current token to check.
     */
    public static void assertIsStartArrayJsonToken(final JsonParser parser) throws JsonParseException {
        if (!(parser.getCurrentToken() == JsonToken.START_ARRAY)) {
            throw new JsonParseException(SR.EXPECTED_START_ARRAY, parser.getCurrentLocation());
        }
    }

    /***
     * Reserved for internal use. Asserts that the token type of the parser is the end of an array.
     *
     * @param parser
     *            The {@link JsonParser} whose current token to check.
     */
    public static void assertIsEndArrayJsonToken(final JsonParser parser) throws JsonParseException {
        if (!(parser.getCurrentToken() == JsonToken.END_ARRAY)) {
            throw new JsonParseException(SR.EXPECTED_END_ARRAY, parser.getCurrentLocation());
        }
    }
}