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

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

/**
 * Represents a set of access conditions to be used for operations against the storage services.
 */
public final class AccessCondition {
    /**
     * Generates a new empty AccessCondition.
     *

     * For more information, see Specifying Conditional Headers
     * for Blob Service Operations.
     *
     * @return An AccessCondition object that has no conditions set.
     */
    public static AccessCondition generateEmptyCondition() {
        return new AccessCondition();
    }

    /**
     * Returns an access condition such that an operation will be performed only if the resource's ETag value matches
     * the specified ETag value.
     *

     * Setting this access condition modifies the request to include the HTTP If-Match conditional header. If
     * this access condition is set, the operation is performed only if the ETag of the resource matches the specified
     * ETag.
     *

     * For more information, see Specifying Conditional Headers
     * for Blob Service Operations.
     *
     * @param etag
     *            A String that represents the ETag value to check.
     *
     * @return An AccessCondition object that represents the If-Match condition.
     */
    public static AccessCondition generateIfMatchCondition(final String etag) {
        AccessCondition retCondition = new AccessCondition();
        retCondition.setIfMatch(etag);
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if the resource has been modified since
     * the specified time.
     *

     * Setting this access condition modifies the request to include the HTTP If-Modified-Since conditional
     * header. If this access condition is set, the operation is performed only if the resource has been modified since
     * the specified time.
     *

     * For more information, see Specifying Conditional Headers
     * for Blob Service Operations.
     *
     * @param lastMotified
     *            A java.util.Date object that represents the last-modified time to check for the resource.
     *
     * @return An AccessCondition object that represents the If-Modified-Since condition.
     */
    public static AccessCondition generateIfModifiedSinceCondition(final Date lastMotified) {
        AccessCondition retCondition = new AccessCondition();
        retCondition.ifModifiedSinceDate = lastMotified;
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if the resource's ETag value does not
     * match the specified ETag value.
     *

     * Setting this access condition modifies the request to include the HTTP If-None-Match conditional header.
     * If this access condition is set, the operation is performed only if the ETag of the resource does not match the
     * specified ETag.
     *

     * For more information, see Specifying Conditional Headers
     * for Blob Service Operations.
     *
     * @param etag
     *            A String that represents the ETag value to check.
     *
     * @return An AccessCondition object that represents the If-None-Match condition.
     */
    public static AccessCondition generateIfNoneMatchCondition(final String etag) {
        AccessCondition retCondition = new AccessCondition();
        retCondition.setIfNoneMatch(etag);
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if the resource has not been modified
     * since the specified time.
     *

     * Setting this access condition modifies the request to include the HTTP If-Unmodified-Since conditional
     * header. If this access condition is set, the operation is performed only if the resource has not been modified
     * since the specified time.
     *

     * For more information, see Specifying Conditional Headers
     * for Blob Service Operations.
     *
     * @param lastMotified
     *            A java.util.Date object that represents the last-modified time to check for the resource.
     *
     * @return An AccessCondition object that represents the If-Unmodified-Since condition.
     */
    public static AccessCondition generateIfNotModifiedSinceCondition(final Date lastMotified) {
        AccessCondition retCondition = new AccessCondition();
        retCondition.ifUnmodifiedSinceDate = lastMotified;
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if resource's current sequence
     * number is less than or equal to the specified value. This condition only applies to page blobs.
     *
     * @param sequenceNumber
     *            The value to compare to the current sequence number.
     *
     * @return An AccessCondition object that represents the If-Sequence-Number-LE condition.
     */
    public static AccessCondition generateIfSequenceNumberLessThanOrEqualCondition(long sequenceNumber) {
        AccessCondition retCondition = new AccessCondition();
        retCondition.ifSequenceNumberLessThanOrEqual = sequenceNumber;
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if resource's current sequence
     * number is less than the specified value. This condition only applies to page blobs.
     *
     * @param sequenceNumber
     *            The value to compare to the current sequence number.
     *
     * @return An AccessCondition object that represents the If-Sequence-Number-LT condition.
     */
    public static AccessCondition generateIfSequenceNumberLessThanCondition(long sequenceNumber) {
        AccessCondition retCondition = new AccessCondition();
        retCondition.ifSequenceNumberLessThan = sequenceNumber;
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if resource's current sequence
     * number is equal to the specified value. This condition only applies to page blobs.
     *
     * @param sequenceNumber
     *            The value to compare to the current sequence number.
     *
     * @return An AccessCondition object that represents the If-Sequence-Number-EQ condition.
     */
    public static AccessCondition generateIfSequenceNumberEqualCondition(long sequenceNumber) {
        AccessCondition retCondition = new AccessCondition();
        retCondition.ifSequenceNumberEqual = sequenceNumber;
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if the resource is accessible under the
     * specified lease ID.
     *

     * For more information, see Specifying Conditional Headers
     * for Blob Service Operations.
     *
     * @param leaseID
     *            The lease ID to specify.
     *
     * @return An AccessCondition object that represents the lease condition.
     */
    public static AccessCondition generateLeaseCondition(final String leaseID) {
        AccessCondition retCondition = new AccessCondition();
        retCondition.leaseID = leaseID;
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if the resource exists on the service.
     *

     * Setting this access condition modifies the request to include the HTTP If-Match conditional header.
     *

     * For more information, see Specifying Conditional Headers
     * for Blob Service Operations.
     *
     * @return An AccessCondition object that represents the if exists condition.
     */
    public static AccessCondition generateIfExistsCondition() {
        AccessCondition retCondition = new AccessCondition();
        retCondition.setIfMatch("*");
        return retCondition;
    }

    /**
     * Returns an access condition such that an operation will be performed only if the resource does not exist on the
     * service.
     *

     * Setting this access condition modifies the request to include the HTTP If-None-Match conditional header.
     *

     * For more information, see Specifying Conditional Headers
     * for Blob Service Operations.
     *
     * @return An AccessCondition object that represents the if not exists condition.
     */
    public static AccessCondition generateIfNotExistsCondition() {
        AccessCondition retCondition = new AccessCondition();
        retCondition.setIfNoneMatch("*");
        return retCondition;
    }

    private String leaseID = null;

    /**
     * Represents the ETag of the resource for if match conditions
     */
    private String ifMatchETag = null;

    /**
     * Represents the ETag of the resource for if none match conditions
     */
    private String ifNoneMatchETag = null;

    /**
     * Represents the date for ifModifiedSinceDate conditions.
     */
    private Date ifModifiedSinceDate = null;

    /**
     * Represents the date for ifUnmodifiedSinceDate conditions.
     */
    private Date ifUnmodifiedSinceDate = null;

    /**
     * Represents the ifSequenceNumberLessThanOrEqual type. Used only for page blob operations.
     */
    private Long ifSequenceNumberLessThanOrEqual = null;

    /**
     * Represents the ifSequenceNumberLessThan type. Used only for page blob operations.
     */
    private Long ifSequenceNumberLessThan = null;

    /**
     * Represents the ifSequenceNumberEqual type. Used only for page blob operations.
     */
    private Long ifSequenceNumberEqual = null;

    /**
     * Represents the ifMaxSizeLessThanOrEqual type. Used only for append blob operations.
     */
    private Long ifMaxSizeLessThanOrEqual = null;

    /**
     * Represents the ifAppendPositionEqual type. Used only for append blob operations.
     */
    private Long ifAppendPositionEqual = null;

    /**
     * Creates an instance of the AccessCondition class.
     */
    public AccessCondition() {
        // Empty default constructor.
    }

    /**
     * RESERVED FOR INTERNAL USE. Applies the access conditions to the request.
     *
     * @param request
     *            A java.net.HttpURLConnection object that represents the request
     *            to which the condition is being applied.
     */
    public void applyConditionToRequest(final HttpURLConnection request) {
        applyLeaseConditionToRequest(request);

        if (this.ifModifiedSinceDate != null) {
            request.setRequestProperty(Constants.HeaderConstants.IF_MODIFIED_SINCE,
                    Utility.getGMTTime(this.ifModifiedSinceDate));
        }

        if (this.ifUnmodifiedSinceDate != null) {
            request.setRequestProperty(Constants.HeaderConstants.IF_UNMODIFIED_SINCE,
                    Utility.getGMTTime(this.ifUnmodifiedSinceDate));
        }

        if (!Utility.isNullOrEmpty(this.ifMatchETag)) {
            request.setRequestProperty(Constants.HeaderConstants.IF_MATCH, this.ifMatchETag);
        }

        if (!Utility.isNullOrEmpty(this.ifNoneMatchETag)) {
            request.setRequestProperty(Constants.HeaderConstants.IF_NONE_MATCH, this.ifNoneMatchETag);
        }
    }

    /**
     * RESERVED FOR INTERNAL USE. Applies the source access conditions to the request.
     *
     * @param request
     *            A java.net.HttpURLConnection object that represents the request
     *            to which the condition is being applied.
     */
    public void applySourceConditionToRequest(final HttpURLConnection request) {
        if (!Utility.isNullOrEmpty(this.leaseID)) {
            // Unsupported
            throw new IllegalArgumentException(SR.LEASE_CONDITION_ON_SOURCE);
        }

        if (this.ifModifiedSinceDate != null) {
            request.setRequestProperty(
                    Constants.HeaderConstants.SOURCE_IF_MODIFIED_SINCE_HEADER,
                    Utility.getGMTTime(this.ifModifiedSinceDate));
        }

        if (this.ifUnmodifiedSinceDate != null) {
            request.setRequestProperty(Constants.HeaderConstants.SOURCE_IF_UNMODIFIED_SINCE_HEADER,
                    Utility.getGMTTime(this.ifUnmodifiedSinceDate));
        }

        if (!Utility.isNullOrEmpty(this.ifMatchETag)) {
            request.setRequestProperty(
                    Constants.HeaderConstants.SOURCE_IF_MATCH_HEADER,
                    this.ifMatchETag);
        }

        if (!Utility.isNullOrEmpty(this.ifNoneMatchETag)) {
            request.setRequestProperty(
                    Constants.HeaderConstants.SOURCE_IF_NONE_MATCH_HEADER,
                    this.ifNoneMatchETag);
        }
    }

    /**
     * RESERVED FOR INTERNAL USE. Applies the access condition to the request.
     *
     * @param request
     *            A java.net.HttpURLConnection object that represents the request to which the condition is
     *            being applied.
     *
     * @throws StorageException
     *             If there is an error parsing the date value of the access condition.
     */
    public void applyAppendConditionToRequest(final HttpURLConnection request) {
        if (this.ifMaxSizeLessThanOrEqual != null) {
            request.setRequestProperty(Constants.HeaderConstants.IF_MAX_SIZE_LESS_THAN_OR_EQUAL,
                    this.ifMaxSizeLessThanOrEqual.toString());
        }

        if (this.ifAppendPositionEqual != null) {
            request.setRequestProperty(Constants.HeaderConstants.IF_APPEND_POSITION_EQUAL_HEADER,
                    this.ifAppendPositionEqual.toString());
        }
    }

    /**
     * RESERVED FOR INTERNAL USE. Applies the lease access condition to the request.
     *
     * @param request
     *            A java.net.HttpURLConnection object that represents the request
     *            to which the condition is being applied.
     */
    public void applyLeaseConditionToRequest(final HttpURLConnection request) {
        if (!Utility.isNullOrEmpty(this.leaseID)) {
            request.setRequestProperty(Constants.HeaderConstants.LEASE_ID_HEADER, this.leaseID);
        }
    }

    /**
     * RESERVED FOR INTERNAL USE. Applies the sequence number access conditions to the request.
     *
     * @param request
     *            A java.net.HttpURLConnection object that represents the request
     *            to which the condition is being applied.
     */
    public void applySequenceConditionToRequest(final HttpURLConnection request) {
        if (this.ifSequenceNumberLessThanOrEqual != null) {
            request.setRequestProperty(
                    Constants.HeaderConstants.IF_SEQUENCE_NUMBER_LESS_THAN_OR_EQUAL,
                    this.ifSequenceNumberLessThanOrEqual.toString());
        }

        if (this.ifSequenceNumberLessThan != null) {
            request.setRequestProperty(
                    Constants.HeaderConstants.IF_SEQUENCE_NUMBER_LESS_THAN,
                    this.ifSequenceNumberLessThan.toString());
        }

        if (this.ifSequenceNumberEqual != null) {
            request.setRequestProperty(
                    Constants.HeaderConstants.IF_SEQUENCE_NUMBER_EQUAL,
                    this.ifSequenceNumberEqual.toString());
        }
    }

    /**
     * Gets the value for a conditional header used only for append operations. A number indicating the byte offset to check for.
     * The append will succeed only if the end position is equal to this number.
     *
     * @return The append position number, or null if no condition exists.
     */
    public Long getIfAppendPositionEqual()
    {
        return ifAppendPositionEqual;
    }

    /**
     * Gets the ETag when the If-Match condition is set.
     *
     * @return The ETag when the If-Match condition is set; otherwise, null.
     */
    public String getIfMatch() {
        return this.ifMatchETag;
    }

    /**
     * Gets the value for a conditional header used only for append operations. A number that indicates the maximum length in
     * bytes to restrict the blob to when committing the block.
     *
     * @return The maximum size, or null if no condition exists.
     */
    public Long getIfMaxSizeLessThanOrEqual()
    {
        return ifMaxSizeLessThanOrEqual;
    }

    /**
     * Gets the If-Modified-Since date.
     *
     * @return A java.util.Date object that represents the If-Modified-Since date.
     */
    public Date getIfModifiedSinceDate() {
        return this.ifModifiedSinceDate;
    }

    /**
     * Gets the ETag when the If-None-Match condition is set.
     *
     * @return The ETag when the If-None-Match condition is set; otherwise, null.
     */
    public String getIfNoneMatch() {
        return this.ifNoneMatchETag;
    }

    /**
     * Gets the If-Unmodified-Since date.
     *
     * @return A java.util.Date object that represents the If-Unmodified-Since date.
     */
    public Date getIfUnmodifiedSinceDate() {
        return this.ifUnmodifiedSinceDate;
    }

    /**
     * Gets the lease ID.
     *
     * @return The lease ID.
     */
    public String getLeaseID() {
        return this.leaseID;
    }

    /**
     * Gets the sequence number when the sequence number less than or equal condition is set. This condition
     * is only applicable to page blobs.
     *
     * @return The sequence number when the ifSequenceNumberLessThanOrEqual condition is set; otherwise, null
     */
    public Long getIfSequenceNumberLessThanOrEqual() {
        return this.ifSequenceNumberLessThanOrEqual;
    }

    /**
     * Gets the sequence number when the sequence number less than condition is set. This condition
     * is only applicable to page blobs.
     *
     * @return The sequence number when the ifSequenceNumberLessThan condition is set; otherwise, null
     */
    public Long getIfSequenceNumberLessThan() {
        return this.ifSequenceNumberLessThan;
    }

    /**
     * Gets the sequence number when the sequence number equal condition is set. This condition
     * is only applicable to page blobs.
     *
     * @return The sequence number when the ifSequenceNumberEqual condition is set; otherwise, null
     */
    public Long getIfSequenceNumberEqual() {
        return this.ifSequenceNumberEqual;
    }

    /**
     * Sets the value for a conditional header used only for append operations. A number indicating the byte offset to check for.
     * The append will succeed only if the end position is equal to this number.
     *
     * @param ifAppendPositionEqual
     *            The append position number, or null if no condition exists.
     */
    public void setIfAppendPositionEqual(Long ifAppendPositionEqual)
    {
        this.ifAppendPositionEqual = ifAppendPositionEqual;
    }

    /**
     * Sets the ETag for the If-Match condition.
     *
     * @param etag
     *            The ETag to set for the If-Match condition.
     */
    public void setIfMatch(String etag) {
        this.ifMatchETag = normalizeEtag(etag);
    }

    /**
     * Sets the value for a conditional header used only for append operations. A number that indicates the maximum length in
     * bytes to restrict the blob to when committing the block.
     *
     * @param ifMaxSizeLessThanOrEqual
     *            The maximum size, or null if no condition exists.
     */
    public void setIfMaxSizeLessThanOrEqual(Long ifMaxSizeLessThanOrEqual)
    {
        this.ifMaxSizeLessThanOrEqual = ifMaxSizeLessThanOrEqual;
    }

    /**
     * Sets the If-Modified-Since date.
     *
     * @param ifModifiedSinceDate
     *            A java.util.Date object that represents the If-Modified-Since to set.
     */
    public void setIfModifiedSinceDate(Date ifModifiedSinceDate) {
        this.ifModifiedSinceDate = ifModifiedSinceDate;
    }

    /**
     * Sets the ETag for the If-None-Match condition.
     *
     * @param etag
     *            The ETag to set for the If-None-Match condition.
     */
    public void setIfNoneMatch(String etag) {
        this.ifNoneMatchETag = normalizeEtag(etag);
    }

    /**
     * Sets the If-Unmodified-Since date.
     *
     * @param ifUnmodifiedSinceDate
     *            A java.util.Date object that represents the If-Unmodified-Since to set.
     */
    public void setIfUnmodifiedSinceDate(Date ifUnmodifiedSinceDate) {
        this.ifUnmodifiedSinceDate = ifUnmodifiedSinceDate;
    }

    /**
     * Gets the lease ID.
     *
     * @param leaseID
     *            The lease ID to set.
     */
    public void setLeaseID(String leaseID) {
        this.leaseID = leaseID;
    }

    /**
     * Sets the sequence number for the sequence number less than or equal to condition. This condition
     * is only applicable to page blobs.
     *
     * @param sequenceNumber
     *            The sequence number to set the if sequence number less than or equal condition to.
     */
    public void setIfSequenceNumberLessThanOrEqual(Long sequenceNumber) {
        this.ifSequenceNumberLessThanOrEqual = sequenceNumber;
    }

    /**
     * Sets the sequence number for the sequence number less than condition. This condition
     * is only applicable to page blobs.
     *
     * @param sequenceNumber
     *            The sequence number to set the if sequence number less than condition to.
     */
    public void setIfSequenceNumberLessThan(Long sequenceNumber) {
        this.ifSequenceNumberLessThan = sequenceNumber;
    }

    /**
     * Sets the sequence number for the sequence number equal to condition. This condition
     * is only applicable to page blobs.
     *
     * @param sequenceNumber
     *            The sequence number to set the if sequence number equal condition to.
     */
    public void setIfSequenceNumberEqual(Long sequenceNumber) {
        this.ifSequenceNumberEqual = sequenceNumber;
    }

    /**
     * RESERVED FOR INTERNAL USE. Verifies the condition is satisfied.
     *
     * @param etag
     *            A String that represents the ETag to check.
     * @param lastModified
     *            A java.util.Date object that represents the last modified date/time.
     *
     * @return true if the condition is satisfied; otherwise, false.
     *
     */
    public boolean verifyConditional(final String etag, final Date lastModified) {
        if (this.ifModifiedSinceDate != null) {
            // The IfModifiedSince has a special helper in HttpURLConnection, use it instead of manually setting the
            // header.
            if (!lastModified.after(this.ifModifiedSinceDate)) {
                return false;
            }
        }

        if (this.ifUnmodifiedSinceDate != null) {
            if (lastModified.after(this.ifUnmodifiedSinceDate)) {
                return false;
            }
        }

        if (!Utility.isNullOrEmpty(this.ifMatchETag)) {
            if (!this.ifMatchETag.equals(etag) && !this.ifMatchETag.equals("*")) {
                return false;
            }
        }

        if (!Utility.isNullOrEmpty(this.ifNoneMatchETag)) {
            if (this.ifNoneMatchETag.equals(etag)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Normalizes an ETag to be quoted, unless it is *.
     *
     * @param inTag
     *            The ETag to normalize.
     * @return The quoted ETag.
     */
    private static String normalizeEtag(String inTag) {
        if (Utility.isNullOrEmpty(inTag) || inTag.equals("*")) {
            return inTag;
        }
        else if (inTag.startsWith("\"") && inTag.endsWith("\"")) {
            return inTag;
        }
        else {
            return String.format("\"%s\"", inTag);
        }
    }
}


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

import java.util.Date;

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

/**
 * Represents the system properties for a share.
 */
public final class FileShareProperties {

    /**
     * Represents the ETag value for the share.
     */
    private String etag;

    /**
     * Represents the share's last-modified time.
     */
    private Date lastModified;

    /**
     * Represents the limit on the size of files (in GB) stored on the share.
     */
    private Integer shareQuota;

    /**
     * Creates an instance of the FileShareProperties class.
     */
    public FileShareProperties() {
    }

    /**
     * Creates an instance of the FileShareProperties class by copying values from another
     * FileShareProperties instance.
     *
     * @param other
     *            A {@link FileShareProperties} object which represents the file share properties to copy.
     */
    public FileShareProperties(final FileShareProperties other) {
        if (other != null) {
            this.setEtag(other.getEtag());
            this.setLastModified(other.getLastModified());
            this.setShareQuota(other.getShareQuota());
        }
    }

    /**
     * Gets the ETag value of the share.
     *

     * The ETag value is a unique identifier that is updated when a write operation is performed against the share. It
     * may be used to perform operations conditionally, providing concurrency control and improved efficiency.
     *

     * The {@link AccessCondition#generateIfMatchCondition(String)} and
     * {@link AccessCondition#generateIfNoneMatchCondition(String)} methods take an ETag value and return an
     * {@link AccessCondition} object that may be specified on the request.
     *
     * @return A String which represents the ETag.
     */
    public String getEtag() {
        return this.etag;
    }

    /**
     * Gets the last modified time on the share.
     *
     * @return A java.util.Date object which represents the last modified time.
     */
    public Date getLastModified() {
        return this.lastModified;
    }

    /**
     * Gets the limit on the size of files (in GB) stored on the share.
     *
     * @return A java.lang.Integer object which represents the limit on
     *            the size of files stored on the share.
     */
    public Integer getShareQuota() {
        return shareQuota;
    }

    /**
     * Sets the ETag value on the share.
     *
     * @param etag
     *            A String which represents the ETag to set.
     */
    protected void setEtag(final String etag) {
        this.etag = etag;
    }

    /**
     * Sets the last modified time on the share.
     *
     * @param lastModified
     *            A java.util.Date object which represents the last modified time to set.
     */
    protected void setLastModified(final Date lastModified) {
        this.lastModified = lastModified;
    }

    /**
     * Sets the limit on the size of files (in GB) stored on the share.
     *
     * @param shareQuota
     *            A java.lang.Integer object which represents the limit on
     *            the size of files stored on the share.
     */
    public void setShareQuota(Integer shareQuota) {
        if (shareQuota != null) {
            Utility.assertInBounds("Share Quota", shareQuota, 1, FileConstants.MAX_SHARE_QUOTA);
        }
        this.shareQuota = shareQuota;
    }
}


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

import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.UriQueryBuilder;
import com.microsoft.azure.storage.core.Utility;

/**
 * A class which represents a query against a specified table. A {@link TableQuery} instance aggregates the query
 * parameters to use when the query is executed. One of the execute or executeSegmented
 * methods of {@link CloudTableClient} must be called to execute the query. The parameters are encoded and passed to the
 * server when the table query is executed.
 *

 * To create a table query with fluent syntax, the {@link #from} static factory method and the {@link #where},
 * {@link #select}, and {@link #take} mutator methods each return a reference to the object which can be chained into a
 * single expression. Use the {@link #from(Class)} static class factory method to create a
 * TableQuery instance that executes on the named table with entities of the specified {@link TableEntity}
 * implementing type. Use the {@link #where} method to specify a filter expression for the entities returned. Use the
 * {@link #select} method to specify the table entity properties to return. Use the {@link #take} method to limit the
 * number of entities returned by the query. Note that nothing prevents calling these methods more than once on a
 * TableQuery, so the values saved in the TableQuery will be the last encountered in order of
 * execution.
 *

 * As an example, you could construct a table query using fluent syntax:
 *

 * TableQuery&ltTableServiceEntity> myQuery = TableQuery.from("Products", DynamicTableEntity.class)

 * &nbsp&nbsp&nbsp&nbsp.where("(PartitionKey eq 'ProductsMNO') and (RowKey ge 'Napkin')")

 * &nbsp&nbsp&nbsp&nbsp.take(25)

 * &nbsp&nbsp&nbsp&nbsp.select(new String[] {"InventoryCount"});
 *

 * This example creates a query on the "Products" table for all entities where the PartitionKey value is "ProductsMNO"
 * and the RowKey value is greater than or equal to "Napkin" and requests the first 25 matching entities, selecting only
 * the common properties and the property named "InventoryCount", and returns them as {@link DynamicTableEntity}
 * objects.
 *

 * Filter expressions for use with the {@link #where} method or {@link #setFilterString} method can be created using
 * fluent syntax with the overloaded {@link #generateFilterCondition} methods and {@link #combineFilters} method, using
 * the comparison operators defined in {@link QueryComparisons} and the logical operators defined in {@link Operators}.
 * Note that the first operand in a filter comparison must be a property name, and the second operand must evaluate to a
 * constant. The PartitionKey and RowKey property values are String types for comparison purposes.
 *

 * The values that may be used in table queries are explained in more detail in the MSDN topic Querying Tables and Entities, but note that
 * the space characters within values do not need to be URL-encoded, as this will be done when the query is executed.
 *

 * The {@link TableQuery#TableQuery(Class)} constructor and {@link TableQuery#from(Class)} static factory methods
 * require a class type which implements {@link TableEntity} and contains a nullary constructor. If the query will be
 * executed using an {@link EntityResolver}, the caller may specify {@link TableServiceEntity} .class as
 * the class type.
 *
 * @param <T>
 *            A class type which implements {@link TableEntity} and contains a nullary constructor. Note: when using an
 *            inner class to define the class type, mark the class as static.
 */
public class TableQuery<T extends TableEntity> {
    /**
     * A static class that maps identifiers to filter expression operators.
     */
    public static class Operators {
        /**
         * And
         */
        public static final String AND = "and";

        /**
         * Not
         */
        public static final String NOT = "not";

        /**
         * Or
         */
        public static final String OR = "or";
    }

    /**
     * A static class that maps identifiers to filter property comparison operators.
     */
    public static class QueryComparisons {
        /**
         * Equal
         */
        public static final String EQUAL = "eq";

        /**
         * Not Equal
         */
        public static final String NOT_EQUAL = "ne";

        /**
         * Greater Than
         */
        public static final String GREATER_THAN = "gt";

        /**
         * Greater Than Or Equal
         */
        public static final String GREATER_THAN_OR_EQUAL = "ge";

        /**
         * Less Than
         */
        public static final String LESS_THAN = "lt";

        /**
         * Less Than Or Equal
         */
        public static final String LESS_THAN_OR_EQUAL = "le";
    }

    /**
     * A static factory method that constructs a {@link TableQuery} instance and defines its table entity type. The
     * method returns the {@link TableQuery} instance reference, allowing additional methods to be chained to modify the
     * query.
     *

     * The created {@link TableQuery} instance is specialized for table entities of the specified class type T. Callers
     * may specify {@link TableServiceEntity} .class as the class type parameter if no more specialized
     * type is required.
     *
     * @param clazzType
     *            The java.lang.Class of the class T implementing the {@link TableEntity}
     *            interface that represents the table entity type for the query.
     *
     * @return
     *         The {@link TableQuery} instance with the entity type specialization set.
     *
     */
    public static <T extends TableEntity> TableQuery<T> from(final Class<T> clazzType) {
        return new TableQuery<T>(clazzType);
    }

    /**
     * Generates a property filter condition string for a boolean value. Creates a formatted string to use
     * in a filter expression that uses the specified operation to compare the property with the value, formatted as a
     * boolean, as in the following example:
     *

     * String condition = generateFilterCondition("BooleanProperty", QueryComparisons.EQUAL, false);
     *

     * This statement sets condition to the following value:
     *

     * BooleanProperty eq false
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A boolean which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final boolean value) {
        return generateFilterCondition(propertyName, operation, value ? Constants.TRUE : Constants.FALSE,
                EdmType.BOOLEAN);
    }

    /**
     * Generates a property filter condition string for a byte[] value. Creates a formatted string to use
     * in a filter expression that uses the specified operation to compare the property with the value, formatted as a
     * binary value, as in the following example:
     *

     * String condition = generateFilterCondition("ByteArray", QueryComparisons.EQUAL, new byte[] {0x01, 0x0f});
     *

     * This statement sets condition to the following value:
     *

     * ByteArray eq X'010f'
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A byte array which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final byte[] value) {
        StringBuilder sb = new StringBuilder();
        Formatter formatter = new Formatter(sb);
        for (byte b : value) {
            formatter.format("%02x", b);
        }
        formatter.flush();
        formatter.close();

        return generateFilterCondition(propertyName, operation, sb.toString(), EdmType.BINARY);
    }

    /**
     * Generates a property filter condition string for a Byte[] value. Creates a formatted string to use
     * in a filter expression that uses the specified operation to compare the property with the value, formatted as a
     * binary value, as in the following example:
     *

     * String condition = generateFilterCondition("ByteArray", QueryComparisons.EQUAL, new Byte[] {0x01, 0xfe});
     *

     * This statement sets condition to the following value:
     *

     * ByteArray eq X'01fe'
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A Byte array which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final Byte[] value) {
        StringBuilder sb = new StringBuilder();
        Formatter formatter = new Formatter(sb);
        for (byte b : value) {
            formatter.format("%02x", b);
        }
        formatter.flush();
        formatter.close();

        return generateFilterCondition(propertyName, operation, sb.toString(), EdmType.BINARY);
    }

    /**
     * Generates a property filter condition string for a java.util.Date value. Creates a formatted string to use in
     * a filter expression that uses the specified operation to compare the property with the value, formatted as a
     * datetime value, as in the following example:
     *

     * String condition = generateFilterCondition("FutureDate", QueryComparisons.GREATER_THAN, new Date());
     *

     * This statement sets condition to something like the following value:
     *

     * FutureDate gt datetime'2013-01-31T09:00:00'
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A java.util.Date which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final Date value) {
        return generateFilterCondition(propertyName, operation,
                Utility.getJavaISO8601Time(value), EdmType.DATE_TIME);
    }

    /**
     * Generates a property filter condition string for a double value. Creates a formatted string to use
     * in a filter expression that uses the specified operation to compare the property with the value, formatted as
     * a double value, as in the following example:
     *

     * String condition = generateFilterCondition("Circumference", QueryComparisons.EQUAL, 2 * 3.141592);
     *

     * This statement sets condition to the following value:
     *

     * Circumference eq 6.283184
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A double which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final double value) {
        return generateFilterCondition(propertyName, operation, Double.toString(value), EdmType.DOUBLE);
    }

    /**
     * Generates a property filter condition string for an int value. Creates a formatted string to use
     * in a filter expression that uses the specified operation to compare the property with the value, formatted as
     * a numeric value, as in the following example:
     *

     * String condition = generateFilterCondition("Population", QueryComparisons.GREATER_THAN, 1000);
     *

     * This statement sets condition to the following value:
     *

     * Population gt 1000
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            An int which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final int value) {
        return generateFilterCondition(propertyName, operation, Integer.toString(value), EdmType.INT32);
    }

    /**
     * Generates a property filter condition string for a long value. Creates a formatted string to use
     * in a filter expression that uses the specified operation to compare the property with the value, formatted as
     * a numeric value, as in the following example:
     *

     * String condition = generateFilterCondition("StellarMass", QueryComparisons.GREATER_THAN, 7000000000L);
     *

     * This statement sets condition to the following value:
     *

     * StellarMass gt 7000000000
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A long which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final long value) {
        return generateFilterCondition(propertyName, operation, Long.toString(value), EdmType.INT64);
    }

    /**
     * Generates a property filter condition string for a String value. Creates a formatted string to use
     * in a filter expression that uses the specified operation to compare the property with the value, formatted as
     * a string value, as in the following example:
     *

     * String condition = generateFilterCondition("Platform", QueryComparisons.EQUAL, "Azure");
     *

     * This statement sets condition to the following value:
     *

     * Platform eq 'Azure'
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A String which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final String value) {
        return generateFilterCondition(propertyName, operation, value, EdmType.STRING);
    }

    /**
     * Generates a property filter condition string. Creates a formatted string to use in a filter expression that uses
     * the specified operation to compare the property with the value, formatted as the specified {@link EdmType}.
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A String which specifies the value to compare with the property.
     * @param edmType
     *            The {@link EdmType} to format the value as.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, String value, EdmType edmType) {
        String valueOperand = null;

        if (edmType == EdmType.BOOLEAN || edmType == EdmType.DOUBLE || edmType == EdmType.INT32) {
            valueOperand = value;
        }
        else if (edmType == EdmType.INT64) {
            valueOperand = String.format("%sL", value);
        }
        else if (edmType == EdmType.DATE_TIME) {
            valueOperand = String.format("datetime'%s'", value);
        }
        else if (edmType == EdmType.GUID) {
            valueOperand = String.format("guid'%s'", value);
        }
        else if (edmType == EdmType.BINARY) {
            valueOperand = String.format("X'%s'", value);
        }
        else {
            valueOperand = String.format("'%s'", value.replace("'", "''"));
        }

        return String.format("%s %s %s", propertyName, operation, valueOperand);
    }

    /**
     * Generates a property filter condition string for a UUID value. Creates a formatted string to use
     * in a filter expression that uses the specified operation to compare the property with the value, formatted as
     * a UUID value, as in the following example:
     *

     * String condition = generateFilterCondition("Identity", QueryComparisons.EQUAL, UUID.fromString(
     * "c9da6455-213d-42c9-9a79-3e9149a57833"));
     *

     * This statement sets condition to the following value:
     *

     * Identity eq guid'c9da6455-213d-42c9-9a79-3e9149a57833'
     *
     * @param propertyName
     *            A String which specifies the name of the property to compare.
     * @param operation
     *            A String which specifies the comparison operator to use.
     * @param value
     *            A UUID which specifies the value to compare with the property.
     * @return
     *         A String which represents the formatted filter condition.
     */
    public static String generateFilterCondition(String propertyName, String operation, final UUID value) {
        return generateFilterCondition(propertyName, operation, value.toString(), EdmType.GUID);
    }

    /**
     * Creates a filter condition using the specified logical operator on two filter conditions.
     *
     * @param filterA
     *            A String which specifies the first formatted filter condition.
     * @param operator
     *            A String which specifies Operators.AND or Operators.OR.
     * @param filterB
     *            A String which specifies the first formatted filter condition.
     * @return
     *         A String which represents the combined filter expression.
     */
    public static String combineFilters(String filterA, String operator, String filterB) {
        return String.format("(%s) %s (%s)", filterA, operator, filterB);
    }

    private Class<T> clazzType = null;
    private String sourceTableName = null;
    private String[] columns = null;
    private Integer takeCount;
    private String filterString = null;

    /**
     * Initializes an empty {@link TableQuery} instance. This table query cannot be executed without
     * setting a table entity type.
     */
    public TableQuery() {
        // empty ctor
    }

    /**
     * Initializes a {@link TableQuery} with the specified table entity type. Callers may specify
     * {@link TableServiceEntity}.class as the class type parameter if no more specialized type is
     * required.
     *
     * @param clazzType
     *            The java.lang.Class of the class T that represents the table entity type for
     *            the query. Class T must be a type that implements {@link TableEntity} and has a nullary
     *            constructor.
     */
    public TableQuery(final Class<T> clazzType) {
        this.setClazzType(clazzType);
    }

    /**
     * Gets the class type of the table entities returned by the query.
     *
     * @return
     *         The java.lang.Class of the class T that represents the table entity type for
     *         the query.
     */
    public Class<T> getClazzType() {
        return this.clazzType;
    }

    /**
     * Gets an array of the table entity property names specified in the table query. All properties in the table are
     * returned by default if no property names are specified with a select clause in the table query. The table entity
     * properties to return may be specified with a call to the {@link #setColumns} or {@link #select} methods with a
     * array of property names as parameter.
     *

     * Note that the system properties PartitionKey, RowKey, and Timestamp are
     * automatically requested from the table service whether specified in the {@link TableQuery} or not.
     *
     * @return
     *         An array of String objects which represents the property names of the table entity properties to
     *         return in the query.
     */
    public String[] getColumns() {
        return this.columns;
    }

    /**
     * Gets the filter expression specified in the table query. All entities in the table are returned by
     * default if no filter expression is specified in the table query. A filter for the entities to return may be
     * specified with a call to the {@link #setFilterString} or {@link #where} methods.
     *
     * @return
     *         A String which represents the filter expression used in the query.
     */
    public String getFilterString() {
        return this.filterString;
    }

    /**
     * Gets the name of the source table specified in the table query.
     *
     * @return
     *         A String which represents the name of the source table used in the query.
     */
    protected String getSourceTableName() {
        return this.sourceTableName;
    }

    /**
     * Gets the number of entities the query returns specified in the table query. If this value is not
     * specified in a table query, a maximum of 1,000 entries will be returned. The number of entities to return may be
     * specified with a call to the {@link #setTakeCount} or {@link #take} methods.
     *

     * If the value returned by getTakeCount is greater than 1,000, the query will throw a
     * {@link StorageException} when executed.
     *
     * @return
     *         An Integer which represents the maximum number of entities for the table query to return.
     */
    public Integer getTakeCount() {
        return this.takeCount;
    }

    /**
     * Defines the property names of the table entity properties to return when the table query is executed. The
     * select clause is optional on a table query, used to limit the table properties returned from the
     * server. By default, a query will return all properties from the table entity.
     *

     * Note that the system properties PartitionKey, RowKey, and Timestamp are
     * automatically requested from the table service whether specified in the {@link TableQuery} or not.
     *
     * @param columns
     *            An array of String objects which specify the property names of the table entity properties
     *            to return when the query is executed.
     *
     * @return
     *         A reference to the {@link TableQuery} instance with the table entity properties to return set.
     */
    public TableQuery<T> select(final String[] columns) {
        this.setColumns(columns);
        return this;
    }

    /**
     * Sets the class type of the table entities returned by the query. A class type is required to execute a table
     * query.
     *

     * Callers may specify {@link TableServiceEntity}.class as the class type parameter if no more
     * specialized type is required.
     *
     * @param clazzType
     *            The java.lang.Class of the class T that represents the table entity type for
     *            the query. Class T must be a type that implements {@link TableEntity} and has a nullary
     *            constructor,
     */
    public void setClazzType(final Class<T> clazzType) {
        Utility.assertNotNull("class type", clazzType);
        Utility.checkNullaryCtor(clazzType);
        this.clazzType = clazzType;
    }

    /**
     * Sets the property names of the table entity properties to return when the table query is executed. By default, a
     * query will return all properties from the table entity.
     *

     * Note that the system properties PartitionKey, RowKey, and Timestamp are
     * automatically requested from the table service whether specified in the {@link TableQuery} or not.
     *
     * @param columns
     *            An array of String objects which specify the property names of the table entity properties
     *            to return when the query is executed.
     */
    public void setColumns(final String[] columns) {
        this.columns = columns;
    }

    /**
     * Sets the filter expression to use in the table query. A filter expression is optional; by default a table query
     * will return all entities in the table.
     *

     * Filter expressions for use with the {@link #setFilterString} method can be created using fluent syntax with the
     * overloaded {@link #generateFilterCondition} methods and {@link #combineFilters} method, using the comparison
     * operators defined in {@link QueryComparisons} and the logical operators defined in {@link Operators}. Note that
     * the first operand in a filter comparison must be a property name, and the second operand must evaluate to a
     * constant. The PartitionKey and RowKey property values are String types for comparison purposes. For
     * example, to query all entities with a PartitionKey value of "AccessLogs" on table query myQuery:
     *

     * &nbsp&nbsp&nbsp&nbspmyQuery.setFilterString("PartitionKey eq 'AccessLogs'");
     *

     * The values that may be used in table queries are explained in more detail in the MSDN topic
     *
     * Querying Tables and Entities, but note
     * that the space characters within values do not need to be URL-encoded, as this will be done when the query is
     * executed.
     *

     * Note that no more than 15 discrete comparisons are permitted within a filter string.
     *
     * @param filterString
     *            A String which represents the filter expression to use in the query.
     */
    public void setFilterString(final String filterString) {
        Utility.assertNotNullOrEmpty("filterString", filterString);
        this.filterString = filterString;
    }

    /**
     * Sets the name of the source table for the table query. A table query must have a source table to be executed.
     *
     * @param sourceTableName
     *            A String which specifies the name of the source table to use in the query.
     */
    protected void setSourceTableName(final String sourceTableName) {
        Utility.assertNotNullOrEmpty("tableName", sourceTableName);
        this.sourceTableName = sourceTableName;
    }

    /**
     * Sets the upper bound for the number of entities the query returns. If this value is not specified in a table
     * query, by default a maximum of 1,000 entries will be returned.
     *

     * If the value specified for the takeCount parameter is greater than 1,000, the query will throw a
     * {@link StorageException} when executed.
     *
     * @param takeCount
     *        An Integer which represents the maximum number of entities for the table query to return.
     */
    public void setTakeCount(final Integer takeCount) {
        if (takeCount != null && takeCount <= 0) {
            throw new IllegalArgumentException(SR.TAKE_COUNT_ZERO_OR_NEGATIVE);
        }

        this.takeCount = takeCount;
    }

    /**
     * Defines the upper bound for the number of entities the query returns. If this value is not specified in a table
     * query, by default a maximum of 1,000 entries will be returned.
     *

     * If the value specified for the take parameter is greater than 1,000, the query will throw a
     * {@link StorageException} when executed.
     *
     * @param take
     *        An Integer which represents the maximum number of entities for the table query to return.
     *
     * @return
     *         A reference to the {@link TableQuery} instance with the number of entities to return set.
     */
    public TableQuery<T> take(final Integer take) {
        if (take != null) {
            this.setTakeCount(take);
        }
        return this;
    }

    /**
     * Defines a filter expression for the table query. Only entities that satisfy the specified filter expression will
     * be returned by the query. Setting a filter expression is optional; by default, all entities in the table are
     * returned if no filter expression is specified in the table query.
     *

     * Filter expressions for use with the {@link #where} method can be created using fluent syntax with the overloaded
     * {@link #generateFilterCondition} methods and {@link #combineFilters} method, using the comparison operators
     * defined in {@link QueryComparisons} and the logical operators defined in {@link Operators}. Note that the first
     * operand in a filter comparison must be a property name, and the second operand must evaluate to a constant. The
     * PartitionKey and RowKey property values are String types for comparison purposes. For example, to
     * query all entities with a PartitionKey value of "AccessLogs" on table query myQuery:
     *

     * &nbsp&nbsp&nbsp&nbspmyQuery = myQuery.where("PartitionKey eq 'AccessLogs'");
     *

     * The values that may be used in table queries are explained in more detail in the MSDN topic
     *
     * Querying Tables and Entities, but note
     * that the space characters within values do not need to be URL-encoded, as this will be done when the query is
     * executed.
     *

     * Note that no more than 15 discrete comparisons are permitted within a filter string.
     *
     * @param filter
     *            A String which specifies the filter expression to apply to the table query.
     * @return
     *         A reference to the {@link TableQuery} instance with the filter on entities to return set.
     */
    public TableQuery<T> where(final String filter) {
        this.setFilterString(filter);
        return this;
    }

    /**
     * Reserved for internal use. Creates a {@link UriQueryBuilder} object representing the table query.
     *
     * @return A {@link UriQueryBuilder} object representing the table query.
     *
     * @throws StorageException
     *             if an error occurs in adding or encoding the query parameters.
     */
    protected UriQueryBuilder generateQueryBuilder() throws StorageException {
        final UriQueryBuilder builder = new UriQueryBuilder();
        if (!Utility.isNullOrEmpty(this.filterString)) {
            builder.add(TableConstants.FILTER, this.filterString);
        }

        if (this.takeCount != null) {
            builder.add(TableConstants.TOP, this.takeCount.toString());
        }

        if (this.columns != null && this.columns.length > 0) {
            final StringBuilder colBuilder = new StringBuilder();

            boolean foundRk = false;
            boolean foundPk = false;
            boolean roundTs = false;

            for (int m = 0; m < this.columns.length; m++) {
                if (TableConstants.ROW_KEY.equals(this.columns[m])) {
                    foundRk = true;
                }
                else if (TableConstants.PARTITION_KEY.equals(this.columns[m])) {
                    foundPk = true;
                }
                else if (TableConstants.TIMESTAMP.equals(this.columns[m])) {
                    roundTs = true;
                }

                colBuilder.append(this.columns[m]);
                if (m < this.columns.length - 1) {
                    colBuilder.append(",");
                }
            }

            if (!foundPk) {
                colBuilder.append(",");
                colBuilder.append(TableConstants.PARTITION_KEY);
            }

            if (!foundRk) {
                colBuilder.append(",");
                colBuilder.append(TableConstants.ROW_KEY);
            }

            if (!roundTs) {
                colBuilder.append(",");
                colBuilder.append(TableConstants.TIMESTAMP);
            }

            builder.add(TableConstants.SELECT, colBuilder.toString());
        }

        return builder;
    }
}


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

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;

import com.microsoft.azure.storage.core.BaseRequest;
import com.microsoft.azure.storage.core.RequestLocationMode;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.StorageRequest;
import com.microsoft.azure.storage.core.StreamMd5AndLength;
import com.microsoft.azure.storage.core.Utility;

/**
 * Provides a client for accessing the Microsoft Azure Storage service.
 */
public abstract class ServiceClient {

    /**
     * Holds the list of URIs for all locations.
     */
    private StorageUri storageUri;

    /**
     * Holds the StorageCredentials associated with this Service Client.
     */
    protected StorageCredentials credentials;

    /**
     * Reserved for internal use. An internal flag which indicates if path style uris should be used.
     */
    private boolean usePathStyleUris;

    /**
     * Creates an instance of the ServiceClient class using the specified service endpoint and account
     * credentials.
     *
     * @param storageUri
     *            A {@link StorageUri} object which represents the service endpoint used to create the client.
     * @param credentials
     *            A {@link StorageCredentials} object which represents the account credentials.
     */
    protected ServiceClient(final StorageUri storageUri, final StorageCredentials credentials) {
        Utility.assertNotNull("baseUri", storageUri);
        if (!storageUri.isAbsolute()) {
            throw new IllegalArgumentException(String.format(SR.RELATIVE_ADDRESS_NOT_PERMITTED, storageUri));
        }

        this.credentials = credentials == null ? StorageCredentialsAnonymous.ANONYMOUS : credentials;
        this.usePathStyleUris = Utility.determinePathStyleFromUri(storageUri.getPrimaryUri());
        this.storageUri = storageUri;
    }

    protected StorageRequest<ServiceClient, Void, ServiceProperties> downloadServicePropertiesImpl(
            final RequestOptions options, final boolean signAsTable) {
        final StorageRequest<ServiceClient, Void, ServiceProperties> getRequest = new StorageRequest<ServiceClient, Void, ServiceProperties>(
                options, this.getStorageUri()) {

            @Override
            public HttpURLConnection buildRequest(ServiceClient client, Void parentObject, OperationContext context)
                    throws Exception {
                return BaseRequest.getServiceProperties(
                        credentials.transformUri(client.getEndpoint()), options, null, context);
            }

            @Override
            public void signRequest(HttpURLConnection connection, ServiceClient client, OperationContext context)
                    throws Exception {
                if (signAsTable) {
                    StorageRequest.signTableRequest(connection, client, -1, context);
                }
                else {
                    StorageRequest.signBlobQueueAndFileRequest(connection, client, -1, context);
                }
            }

            @Override
            public ServiceProperties preProcessResponse(Void parentObject, ServiceClient client,
                    OperationContext context) throws Exception {
                if (this.getResult().getStatusCode() != HttpURLConnection.HTTP_OK) {
                    this.setNonExceptionedRetryableFailure(true);
                }

                return null;
            }

            @Override
            public ServiceProperties postProcessResponse(HttpURLConnection connection, Void parentObject,
                    ServiceClient client, OperationContext context, ServiceProperties storageObject) throws Exception {
                return ServicePropertiesHandler.readServicePropertiesFromStream(connection.getInputStream());
            }
        };

        return getRequest;
    }

    protected StorageRequest<ServiceClient, Void, ServiceStats> getServiceStatsImpl(final RequestOptions options,
            final boolean signAsTable) {
        final StorageRequest<ServiceClient, Void, ServiceStats> getRequest = new StorageRequest<ServiceClient, Void, ServiceStats>(
                options, this.getStorageUri()) {

            @Override
            public void setRequestLocationMode() {
                this.applyLocationModeToRequest();
                this.setRequestLocationMode(RequestLocationMode.PRIMARY_OR_SECONDARY);
            }

            @Override
            public HttpURLConnection buildRequest(ServiceClient client, Void parentObject, OperationContext context)
                    throws Exception {
                return BaseRequest.getServiceStats(
                        credentials.transformUri(client.getStorageUri().getUri(this.getCurrentLocation())),
                        options, null, context);
            }

            @Override
            public void signRequest(HttpURLConnection connection, ServiceClient client, OperationContext context)
                    throws Exception {
                if (signAsTable) {
                    StorageRequest.signTableRequest(connection, client, -1, context);
                }
                else {
                    StorageRequest.signBlobQueueAndFileRequest(connection, client, -1, context);
                }
            }

            @Override
            public ServiceStats preProcessResponse(Void parentObject, ServiceClient client, OperationContext context)
                    throws Exception {
                if (this.getResult().getStatusCode() != HttpURLConnection.HTTP_OK) {
                    this.setNonExceptionedRetryableFailure(true);
                }

                return null;
            }

            @Override
            public ServiceStats postProcessResponse(HttpURLConnection connection, Void parentObject,
                    ServiceClient client, OperationContext context, ServiceStats storageObject) throws Exception {
                return ServiceStatsHandler.readServiceStatsFromStream(connection.getInputStream());
            }

        };

        return getRequest;
    }

    /**
     * Returns the storage credentials associated with this service client.
     *
     * @return A {@link StorageCredentials} object which represents the storage credentials associated with this client.
     */
    public final StorageCredentials getCredentials() {
        return this.credentials;
    }

    /**
     * Returns the base URI for this service client.
     *
     * @return A java.net.URI object which represents the base URI for the service client.
     */
    public final URI getEndpoint() {
        return this.storageUri.getPrimaryUri();
    }

    /**
     * Returns the list of URIs for all locations.
     *
     * @return A {@link StorageUri} object which represents the list of URIs for all locations.
     */
    public final StorageUri getStorageUri() {
        return this.storageUri;
    }

    /**
     * @return true if path-style URIs are used; otherwise, false.
     */
    protected boolean isUsePathStyleUris() {
        return this.usePathStyleUris;
    }

    /**
     * Sets the credentials to use with this service client.
     *
     * @param credentials
     *            A {@link StorageCredentials} object which represents the credentials being assigned for the service
     *            client.
     */
    protected final void setCredentials(final StorageCredentials credentials) {
        this.credentials = credentials;
    }

    /**
     * Sets the list of URIs for all locations.
     *
     * @param storageUri
     *            A {@link StorageUri} object which represents the list of URIs for all locations.
     */
    protected final void setStorageUri(final StorageUri storageUri) {
        this.usePathStyleUris = Utility.determinePathStyleFromUri(storageUri.getPrimaryUri());
        this.storageUri = storageUri;
    }

    protected StorageRequest<ServiceClient, Void, Void> uploadServicePropertiesImpl(final ServiceProperties properties,
            final RequestOptions options, final OperationContext opContext, final boolean signAsTable)
            throws StorageException {
        try {
            byte[] propertiesBytes = ServicePropertiesSerializer.serializeToByteArray(properties);

            final ByteArrayInputStream sendStream = new ByteArrayInputStream(propertiesBytes);
            final StreamMd5AndLength descriptor = Utility.analyzeStream(sendStream, -1L, -1L,
                    true /* rewindSourceStream */, true /* calculateMD5 */);

            final StorageRequest<ServiceClient, Void, Void> putRequest = new StorageRequest<ServiceClient, Void, Void>(
                    options, this.getStorageUri()) {

                @Override
                public HttpURLConnection buildRequest(ServiceClient client, Void parentObject, OperationContext context)
                        throws Exception {
                    this.setSendStream(sendStream);
                    this.setLength(descriptor.getLength());
                    return BaseRequest.setServiceProperties(
                            credentials.transformUri(client.getEndpoint()), options, null, context);
                }

                @Override
                public void setHeaders(HttpURLConnection connection, Void parentObject, OperationContext context) {
                    connection.setRequestProperty(Constants.HeaderConstants.CONTENT_MD5, descriptor.getMd5());
                }

                @Override
                public void signRequest(HttpURLConnection connection, ServiceClient client, OperationContext context)
                        throws Exception {
                    if (signAsTable) {
                        StorageRequest.signTableRequest(connection, client, descriptor.getLength(), context);
                    }
                    else {
                        StorageRequest.signBlobQueueAndFileRequest(connection, client, descriptor.getLength(), context);
                    }
                }

                @Override
                public Void preProcessResponse(Void parentObject, ServiceClient client, OperationContext context)
                        throws Exception {
                    if (this.getResult().getStatusCode() != HttpURLConnection.HTTP_ACCEPTED) {
                        this.setNonExceptionedRetryableFailure(true);
                    }

                    return null;
                }

                @Override
                public void recoveryAction(OperationContext context) throws IOException {
                    sendStream.reset();
                    sendStream.mark(Constants.MAX_MARK_LENGTH);
                }
            };

            return putRequest;
        }
        catch (IllegalArgumentException e) {
            // The request was not even made. There was an error while trying to write the message. Just throw.
            StorageException translatedException = StorageException.translateClientException(e);
            throw translatedException;
        }
        catch (IllegalStateException e) {
            // The request was not even made. There was an error while trying to write the message. Just throw.
            StorageException translatedException = StorageException.translateClientException(e);
            throw translatedException;
        }
        catch (IOException e) {
            // The request was not even made. There was an error while trying to write the message. Just throw.
            StorageException translatedException = StorageException.translateClientException(e);
            throw translatedException;
        }
    }

    /**
     * Gets the {@link RequestOptions} that is used for requests associated with this ServiceClient
     *
     * @return The {@link RequestOptions} object containing the values used by this ServiceClient
     */
    public abstract RequestOptions getDefaultRequestOptions();
}


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

import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map.Entry;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.core.Utility;

/**
 * RESERVED FOR INTERNAL USE. A class used to serialize table entities to a byte array.
 */
final class TableEntitySerializer {
    /**
     * Used to create Json parsers and generators.
     */
    private static JsonFactory jsonFactory = new JsonFactory();

    /**
     * Reserved for internal use. Writes an entity to the stream as an JSON resource, leaving the stream open
     * for additional writing.
     *
     * @param outStream
     *            The OutputStream to write the entity to..
     * @param entity
     *            The instance implementing {@link TableEntity} to write to the output stream.
     * @param isTableEntry
     *            A flag indicating the entity is a reference to a table at the top level of the storage service when
     *            true and a reference to an entity within a table when false.
     * @param opContext
     *            An {@link OperationContext} object used to track the execution of the operation.
     *
     * @throws StorageException
     *             if a Storage service error occurs.
     * @throws IOException
     *             if an error occurs while accessing the stream.
     */
    static void writeSingleEntity(final OutputStream outStream, final TableEntity entity, final boolean isTableEntry,
            final OperationContext opContext) throws StorageException, IOException {
        JsonGenerator generator = jsonFactory.createGenerator(outStream);

        try {
            // write to stream
            writeJsonEntity(generator, entity, isTableEntry, opContext);
        }
        finally {
            generator.close();
        }
    }

    /**
     * Reserved for internal use. Writes an entity to the stream as an JSON resource, leaving the stream open
     * for additional writing.
     *
     * @param strWriter
     *            The StringWriter to write the entity to.
     * @param entity
     *            The instance implementing {@link TableEntity} to write to the output stream.
     * @param isTableEntry
     *            A flag indicating the entity is a reference to a table at the top level of the storage service when
     *            true and a reference to an entity within a table when false.
     * @param opContext
     *            An {@link OperationContext} object used to track the execution of the operation.
     *
     * @throws StorageException
     *             if a Storage service error occurs.
     * @throws IOException
     *             if an error occurs while accessing the stream.
     */
    protected static void writeSingleEntity(final StringWriter strWriter, final TableEntity entity,
            final boolean isTableEntry, final OperationContext opContext) throws StorageException, IOException {
        JsonGenerator generator = jsonFactory.createGenerator(strWriter);

        try {
            // write to stream
            writeJsonEntity(generator, entity, isTableEntry, opContext);
        }
        finally {
            generator.close();
        }
    }

    /**
     * Reserved for internal use. Writes an entity to the specified JsonGenerator as an JSON resource
     *
     * @param generator
     *            The JsonGenerator to write the entity to.
     * @param entity
     *            The instance implementing {@link TableEntity} to write to the output stream.
     * @param isTableEntry
     *            A flag indicating the entity is a reference to a table at the top level of the storage service when
     *            true and a reference to an entity within a table when false.
     * @param opContext
     *            An {@link OperationContext} object used to track the execution of the operation.
     *
     * @throws StorageException
     *             if a Storage service error occurs.
     * @throws IOException
     *             if an error occurs while accessing the stream.
     */
    private static void writeJsonEntity(final JsonGenerator generator, final TableEntity entity,
            final boolean isTableEntry, final OperationContext opContext) throws StorageException, IOException {

        HashMap<String, EntityProperty> properties = entity.writeEntity(opContext);
        if (properties == null) {
            properties = new HashMap<String, EntityProperty>();
        }

        // start object
        generator.writeStartObject();

        if (!isTableEntry) {
            Utility.assertNotNull(TableConstants.PARTITION_KEY, entity.getPartitionKey());
            Utility.assertNotNull(TableConstants.ROW_KEY, entity.getRowKey());
            Utility.assertNotNull(TableConstants.TIMESTAMP, entity.getTimestamp());

            // PartitionKey
            generator.writeStringField(TableConstants.PARTITION_KEY, entity.getPartitionKey());

            // RowKey
            generator.writeStringField(TableConstants.ROW_KEY, entity.getRowKey());

            // Timestamp
            generator.writeStringField(TableConstants.TIMESTAMP, Utility.getJavaISO8601Time(entity.getTimestamp()));
        }

        for (final Entry<String, EntityProperty> ent : properties.entrySet()) {
            if (ent.getKey().equals(TableConstants.PARTITION_KEY) || ent.getKey().equals(TableConstants.ROW_KEY)
                    || ent.getKey().equals(TableConstants.TIMESTAMP) || ent.getKey().equals("Etag")) {
                continue;
            }

            EntityProperty currProp = ent.getValue();
            if (currProp.getEdmType().mustAnnotateType()) {
                final String edmTypeString = currProp.getEdmType().toString();

                // property type
                generator.writeStringField(ent.getKey() + ODataConstants.ODATA_TYPE_SUFFIX, edmTypeString);

                // property key and value
                generator.writeStringField(ent.getKey(), ent.getValue().getValueAsString());
            }
            else if (currProp.getEdmType() == EdmType.DOUBLE && currProp.getIsNull() == false) {
                final String edmTypeString = currProp.getEdmType().toString();
                final Double value = currProp.getValueAsDouble();

                // property type, if needed
                if (value.equals(Double.POSITIVE_INFINITY) || value.equals(Double.NEGATIVE_INFINITY)
                        || value.equals(Double.NaN)) {
                    generator.writeStringField(ent.getKey() + ODataConstants.ODATA_TYPE_SUFFIX, edmTypeString);

                    // property key and value
                    generator.writeStringField(ent.getKey(), ent.getValue().getValueAsString());
                }
                else {
                    writeJsonProperty(generator, ent);
                }

            }
            else {
                writeJsonProperty(generator, ent);
            }
        }

        // end object
        generator.writeEndObject();
    }

    private static void writeJsonProperty(JsonGenerator generator, Entry<String, EntityProperty> prop)
            throws JsonGenerationException, IOException {
        EdmType edmType = prop.getValue().getEdmType();
        if (prop.getValue().getIsNull()) {
            generator.writeNullField(prop.getKey());
        }
        else if (edmType == EdmType.BOOLEAN) {
            generator.writeBooleanField(prop.getKey(), prop.getValue().getValueAsBoolean());
        }
        else if (edmType == EdmType.DOUBLE) {
            generator.writeNumberField(prop.getKey(), prop.getValue().getValueAsDouble());
        }
        else if (edmType == EdmType.INT32) {
            generator.writeNumberField(prop.getKey(), prop.getValue().getValueAsInteger());
        }
        else {
            generator.writeStringField(prop.getKey(), prop.getValue().getValueAsString());
        }
    }
}


/**
 * 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 com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.Utility;

import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map.Entry;

/**
 * RESERVED FOR INTERNAL USE. A class used to serialize SharedAccessPolicies to a byte array.
 */
public final class SharedAccessPolicySerializer {
    /**
     * RESERVED FOR INTERNAL USE. Writes a collection of shared access policies to the specified stream in XML format.
     *
     * @param <T>
     *
     * @param sharedAccessPolicies
     *            A collection of shared access policies
     * @param outWriter
     *            a sink to write the output to.
     * @throws IOException
     *             if there is an error writing the shared access identifiers.
     * @throws IllegalStateException
     *             if there is an error writing the shared access identifiers.
     * @throws IllegalArgumentException
     *             if there is an error writing the shared access identifiers.
     */
    public static <T extends SharedAccessPolicy> void writeSharedAccessIdentifiersToStream(
            final HashMap<String, T> sharedAccessPolicies, final StringWriter outWriter)
            throws IllegalArgumentException, IllegalStateException, IOException {
        Utility.assertNotNull("sharedAccessPolicies", sharedAccessPolicies);
        Utility.assertNotNull("outWriter", outWriter);

        final XmlSerializer xmlw = Utility.getXmlSerializer(outWriter);

        if (sharedAccessPolicies.keySet().size() > Constants.MAX_SHARED_ACCESS_POLICY_IDENTIFIERS) {
            final String errorMessage = String.format(SR.TOO_MANY_SHARED_ACCESS_POLICY_IDENTIFIERS,
                    sharedAccessPolicies.keySet().size(), Constants.MAX_SHARED_ACCESS_POLICY_IDENTIFIERS);

            throw new IllegalArgumentException(errorMessage);
        }

        // default is UTF8
        xmlw.startDocument(Constants.UTF8_CHARSET, true);
        xmlw.startTag(Constants.EMPTY_STRING, Constants.SIGNED_IDENTIFIERS_ELEMENT);

        for (final Entry<String, T> entry : sharedAccessPolicies.entrySet()) {
            final SharedAccessPolicy policy = entry.getValue();
            xmlw.startTag(Constants.EMPTY_STRING, Constants.SIGNED_IDENTIFIER_ELEMENT);

            // Set the identifier
            Utility.serializeElement(xmlw, Constants.ID, entry.getKey());

            xmlw.startTag(Constants.EMPTY_STRING, Constants.ACCESS_POLICY);

            // Set the Start Time
            Utility.serializeElement(xmlw, Constants.START, Utility
                    .getUTCTimeOrEmpty(policy.getSharedAccessStartTime()).toString());

            // Set the Expiry Time
            Utility.serializeElement(xmlw, Constants.EXPIRY,
                    Utility.getUTCTimeOrEmpty(policy.getSharedAccessExpiryTime()).toString());

            // Set the Permissions
            Utility.serializeElement(xmlw, Constants.PERMISSION, policy.permissionsToString());

            // end AccessPolicy
            xmlw.endTag(Constants.EMPTY_STRING, Constants.ACCESS_POLICY);
            // end SignedIdentifier
            xmlw.endTag(Constants.EMPTY_STRING, Constants.SIGNED_IDENTIFIER_ELEMENT);
        }

        // end SignedIdentifiers
        xmlw.endTag(Constants.EMPTY_STRING, Constants.SIGNED_IDENTIFIERS_ELEMENT);
        // end doc
        xmlw.endDocument();
    }
}


/**
 * 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 java.io.IOException;
import java.io.StringWriter;

import org.xmlpull.v1.XmlSerializer;

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

/**
 * RESERVED FOR INTERNAL USE. A class used to serialize message requests to a byte array.
 */
final class QueueMessageSerializer {

    /**
     * Generates the message request body from a string containing the message.
     * The message must be encodable as UTF-8. To be included in a web request,
     * this message request body must be written to the output stream of the web
     * request.
     *
     * @param message
     *            A String containing the message to wrap in a message request body.
     *
     * @return An array of byte containing the message request body
     *         encoded as UTF-8.
     * @throws IOException
     *             if there is an error writing the queue message.
     * @throws IllegalStateException
     *             if there is an error writing the queue message.
     * @throws IllegalArgumentException
     *             if there is an error writing the queue message.
     */
    public static byte[] generateMessageRequestBody(final String message) throws IllegalArgumentException,
            IllegalStateException, IOException {
        final StringWriter outWriter = new StringWriter();
        final XmlSerializer xmlw = Utility.getXmlSerializer(outWriter);

        // default is UTF8
        xmlw.startDocument(Constants.UTF8_CHARSET, true);
        xmlw.startTag(Constants.EMPTY_STRING, QueueConstants.QUEUE_MESSAGE_ELEMENT);

        Utility.serializeElement(xmlw, QueueConstants.MESSAGE_TEXT_ELEMENT, message);

        // end QueueMessage_ELEMENT
        xmlw.endTag(Constants.EMPTY_STRING, QueueConstants.QUEUE_MESSAGE_ELEMENT);

        // end doc
        xmlw.endDocument();

        return outWriter.toString().getBytes(Constants.UTF8_CHARSET);
    }
}


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

package com.microsoft.azure.storage.table;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Date;
import java.util.HashMap;

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

/**
 * A class which represents the result of a table operation. The {@link TableResult} class encapsulates the HTTP
 * response
 * and any table entity results returned by the Storage Service REST API operation called for a particular
 * {@link TableOperation}.
 *
 */
public class TableResult {
    private Object result;

    private int httpStatusCode = -1;

    private String etag;

    private HashMap<String, EntityProperty> properties;

    /**
     * Initializes an empty {@link TableResult} instance.
     */
    public TableResult() {
        // Empty constructor.
    }

    /**
     * Initializes a {@link TableResult} instance with the specified HTTP status code.
     *
     * @param httpStatusCode
     *            An int which represents the HTTP status code for the table operation returned by the server.
     */
    public TableResult(final int httpStatusCode) {
        this.httpStatusCode = httpStatusCode;
    }

    /**
     * Gets the ETag returned with the table operation results. The server will return the same ETag value for a
     * table, entity, or entity group returned by an operation as long as it is unchanged on the server.
     *
     * @return
     *         A String containing the ETag returned by the server with the table operation results.
     */
    public String getEtag() {
        return this.etag;
    }

    /**
     * Gets the HTTP status code returned by a table operation request.
     *
     * @return
     *         An int which represents the HTTP status code for the table operation returned by the server.
     */
    public int getHttpStatusCode() {
        return this.httpStatusCode;
    }

    /**
     * Gets the map of properties for a table entity returned by the table operation.
     *
     * @return
     *         A java.util.HashMap of String property names to {@link EntityProperty} data
     *         typed values representing the properties of a table entity.
     */
    public HashMap<String, EntityProperty> getProperties() {
        return this.properties;
    }

    /**
     * Gets the result returned by the table operation as an Object.
     *
     * @return
     *         An Object reference which represents the result returned by the table operation.
     */
    public Object getResult() {
        return this.result;
    }

    /**
     * Gets the result returned by the table operation as an instance of the specified type.
     *
     * @return
     *         An T reference which represents the result returned by the table operation.
     */
    @SuppressWarnings("unchecked")
    public <T> T getResultAsType() {
        return (T) this.getResult();
    }

    /**
     * Reserved for internal use. Sets the ETag associated with the table operation results.
     *
     * @param etag
     *            A String containing an ETag to associate with the table operation results.
     */
    protected void setEtag(final String etag) {
        this.etag = etag;
    }

    /**
     * Reserved for internal use. Sets the HTTP status code associated with the table operation results.
     *
     * @param httpStatusCode
     *            The HTTP status code value to associate with the table operation results.
     */
    protected void setHttpStatusCode(final int httpStatusCode) {
        this.httpStatusCode = httpStatusCode;
    }

    /**
     * Reserved for internal use. Sets the map of properties for a table entity to associate with the table operation.
     *
     * @param properties
     *            A java.util.HashMap of String property names to {@link EntityProperty} data
     *            typed values representing the properties of a table entity to associate with the table operation.
     */
    protected void setProperties(final HashMap<String, EntityProperty> properties) {
        this.properties = properties;
    }

    /**
     * Reserved for internal use. Sets a result Object instance to associate with the table operation.
     *
     * @param result
     *            An instance of a result Object to associate with the table operation.
     */
    protected void setResult(final Object result) {
        this.result = result;
    }

    /**
     * Reserved for internal use. Sets the result to associate with the table operation as a {@link TableEntity}.
     *
     * @param ent
     *            An instance of an object implementing {@link TableEntity} to associate with the table operation.
     * @throws UnsupportedEncodingException
     */
    protected void updateResultObject(final TableEntity ent) throws UnsupportedEncodingException {
        this.result = ent;
        ent.setEtag(this.etag);
        if (this.etag != null) {
            ent.setTimestamp(parseETagForTimestamp(this.etag));
        }
    }

    private static Date parseETagForTimestamp(String etag) throws UnsupportedEncodingException {
        // must handle both weak and strong ETags
        if (etag.startsWith("W/")) {
            etag = etag.substring(2);
        }

        // remove etag prefix and '" suffix
        etag = etag.substring(TableConstants.ETAG_PREFIX.length(), etag.length() - 2);

        etag = URLDecoder.decode(etag, Constants.UTF8_CHARSET);

        return Utility.parseDate(etag);
    }
}