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

package com.microsoft.azure.storage.core;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.security.InvalidKeyException;

import com.microsoft.azure.storage.AccessCondition;
import com.microsoft.azure.storage.LocationMode;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.RequestOptions;
import com.microsoft.azure.storage.RequestResult;
import com.microsoft.azure.storage.ServiceClient;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.StorageExtendedErrorInformation;
import com.microsoft.azure.storage.StorageLocation;
import com.microsoft.azure.storage.StorageUri;

/**
 * RESERVED FOR INTERNAL USE. A class which encapsulate the execution of a given storage operation.
 *
 * @param <C>
 *            The service client type
 * @param <P>
 *            The type of the parent object, i.e. CloudBlobContainer for downloadAttributes etc.
 * @param <R>
 *            The type of the expected result
 */
public abstract class StorageRequest<C, P, R> {
    /**
     * Holds a reference to a realized exception which occurred during execution.
     */
    private StorageException exceptionReference;

    /**
     * A flag to indicate a failure which did not result in an exception, i.e a 400 class status code.
     */
    private boolean nonExceptionedRetryableFailure;

    /**
     * The RequestOptions to use for the request.
     */
    private RequestOptions requestOptions;

    /**
     * Holds the result for the operation.
     */
    private RequestResult result;

    /**
     * Holds the url connection for the operation.
     */
    private HttpURLConnection connection;

    private InputStream sendStream;

    /**
     * Holds the blob/file offset for recovery action.
     */
    private Long offset = null;

    /**
     * Holds the length
     */
    private Long length = null;

    /**
     * Holds the locked ETag
     */
    private String lockedETag = null;

    /**
     * Holds the locked ETag condition
     */
    private AccessCondition etagLockCondition = null;

    /**
     * Denotes whether properties have been populated or not.
     */
    private boolean arePropertiesPopulated = false;

    /**
     * Holds the ContentMD5 for retries.
     */
    private String contentMD5 = null;

    /**
     * Holds the descriptor which contains the stream length and MD5 hash.
     */
    private StreamMd5AndLength currentDescriptor = null;

    /**
     * Denotes the StorageUri of the request
     */
    private StorageUri storageUri;

    /**
     * Location mode set by the user
     */
    private LocationMode locationMode;

    /**
     * Location mode of the request
     */
    private RequestLocationMode requestLocationMode;

    /**
     * Current location of the request
     */
    private StorageLocation currentLocation;

    /**
     * Total number of bytes read so far
     */
    private long currentRequestByteCount = 0;

    /**
     * Denotes whether the associated request has been sent.
     */
    private boolean isSent = false;

    /**
     * Default Ctor.
     */
    protected StorageRequest() {
        // no op
    }

    /**
     * Initializes a new instance of the StorageRequest class.
     *
     * @param options
     *            the RequestOptions to use
     */
    public StorageRequest(final RequestOptions options, StorageUri storageUri) {
        this.setRequestOptions(options);
        this.setStorageUri(storageUri);
        this.locationMode = LocationMode.PRIMARY_ONLY;
        this.requestLocationMode = RequestLocationMode.PRIMARY_ONLY;
    }

    /**
     * @return the exception
     */
    public final StorageException getException() {
        return this.exceptionReference;
    }

    /**
     * @return the requestOptions
     */
    public final RequestOptions getRequestOptions() {
        return this.requestOptions;
    }

    /**
     * @return the result
     */
    public final RequestResult getResult() {
        return this.result;
    }

    /**
     * @return the URL connection
     */
    public final HttpURLConnection getConnection() {
        return this.connection;
    }

    /**
     * @return the stream to send to server
     */
    public final InputStream getSendStream() {
        return this.sendStream;
    }

    /**
     * @return the offset to start reading from
     */
    public Long getOffset() {
        return this.offset;
    }

    /**
     * @return the length, in bytes, of the stream
     */
    public Long getLength() {
        return this.length;
    }

    /**
     * @return the locked ETag
     */
    public final String getLockedETag() {
        return this.lockedETag;
    }

    /**
     * @return the ContentMD5
     */
    public final String getContentMD5() {
        return this.contentMD5;
    }

    /**
     * @return the current descriptor which contains the stream length and MD5 hash.
     */
    protected StreamMd5AndLength getCurrentDescriptor() { return this.currentDescriptor; }

    /**
     * @return the location mode used to decide which location the request should be sent to.
     */
    public LocationMode getLocationMode() {
        return this.locationMode;
    }

    /**
     * @return the location mode used to decide which location the request should be sent to.
     */
    public RequestLocationMode getRequestLocationMode() {
        return this.requestLocationMode;
    }

    /**
     * @return the current location to which the request will be sent.
     */
    public StorageLocation getCurrentLocation() {
        return this.currentLocation;
    }

    /**
     * @return the locked ETag condition
     */
    public AccessCondition getETagLockCondition() {
        return this.etagLockCondition;
    }

    /**
     * @return the arePropertiesPopulated value
     */
    public boolean getArePropertiesPopulated() {
        return this.arePropertiesPopulated;
    }

    /**
     * @return the URI to which the request will be sent.
     */
    public StorageUri getStorageUri() {
        return this.storageUri;
    }

    /**
     * @return the currentRequestByteCount
     */
    public long getCurrentRequestByteCount() {
        return this.currentRequestByteCount;
    }

    /**
     * @return the isSent value
     */
    protected boolean isSent() {
        return this.isSent;
    }

    /**
     * Resets the operation status flags between operations.
     */
    protected final void initialize(OperationContext opContext) {
        RequestResult currResult = new RequestResult();
        this.setResult(currResult);
        opContext.appendRequestResult(currResult);

        this.setException(null);
        this.setNonExceptionedRetryableFailure(false);
        this.setIsSent(false);
    }

    /**
     * @return the nonExceptionedRetryableFailure
     */
    public final boolean isNonExceptionedRetryableFailure() {
        return this.nonExceptionedRetryableFailure;
    }

    /**
     * Returns either the held exception from the operation if it is set, otherwise the translated exception.
     *
     * @param request
     *            the reference to the HttpURLConnection for the operation.
     * @param opContext
     *            an object used to track the execution of the operation
     * @return the exception to throw.
     */
    protected final StorageException materializeException(final OperationContext opContext) {
        if (this.getException() != null) {
            return this.getException();
        }

        return StorageException.translateException(this, null, opContext);
    }

    public static final void signBlobQueueAndFileRequest(HttpURLConnection request, ServiceClient client,
            long contentLength, OperationContext context) throws InvalidKeyException, StorageException {
        StorageCredentialsHelper.signBlobQueueAndFileRequest(client.getCredentials(), request, contentLength, context);
    }

    public static final void signTableRequest(HttpURLConnection request, ServiceClient client, long contentLength,
            OperationContext context) throws InvalidKeyException, StorageException {
        StorageCredentialsHelper.signTableRequest(client.getCredentials(), request, contentLength, context);
    }

    public void applyLocationModeToRequest() {
        if (this.getRequestOptions().getLocationMode() != null) {
            this.setLocationMode(this.getRequestOptions().getLocationMode());
        }
    }

    public void initializeLocation() {
        if (this.getStorageUri() != null) {
            switch (this.getLocationMode()) {
                case PRIMARY_ONLY:
                case PRIMARY_THEN_SECONDARY:
                    this.setCurrentLocation(StorageLocation.PRIMARY);
                    break;

                case SECONDARY_ONLY:
                case SECONDARY_THEN_PRIMARY:
                    this.setCurrentLocation(StorageLocation.SECONDARY);
                    break;

                default:
                    throw new IllegalArgumentException(String.format(SR.ARGUMENT_OUT_OF_RANGE_ERROR, "locationMode",
                            this.getLocationMode()));
            }
        }
        else {
            this.setCurrentLocation(StorageLocation.PRIMARY);
        }
    }

    @SuppressWarnings("incomplete-switch")
    public void validateLocation() {
        if (this.getStorageUri() != null) {
            if (!this.getStorageUri().validateLocationMode(this.locationMode)) {
                throw new UnsupportedOperationException(SR.STORAGE_URI_MISSING_LOCATION);
            }
        }

        // If the command only allows for a specific location, we should target
        // that location no matter what the retry policy says.
        switch (this.getRequestLocationMode()) {
            case PRIMARY_ONLY:
                if (this.getLocationMode() == LocationMode.SECONDARY_ONLY) {
                    throw new IllegalArgumentException(SR.PRIMARY_ONLY_COMMAND);
                }

                this.setCurrentLocation(StorageLocation.PRIMARY);
                this.setLocationMode(LocationMode.PRIMARY_ONLY);
                break;

            case SECONDARY_ONLY:
                if (this.getLocationMode() == LocationMode.PRIMARY_ONLY) {
                    throw new IllegalArgumentException(SR.SECONDARY_ONLY_COMMAND);
                }

                this.setCurrentLocation(StorageLocation.SECONDARY);
                this.setLocationMode(LocationMode.SECONDARY_ONLY);
                break;
        }

        this.getResult().setTargetLocation(this.currentLocation);
    }

    /**
     * @param exceptionReference
     *            the exception to set
     */
    protected final void setException(final StorageException exceptionReference) {
        this.exceptionReference = exceptionReference;
    }

    /**
     * @param nonExceptionedRetryableFailure
     *            the nonExceptionedRetryableFailure to set
     */
    public final void setNonExceptionedRetryableFailure(final boolean nonExceptionedRetryableFailure) {
        this.nonExceptionedRetryableFailure = nonExceptionedRetryableFailure;
    }

    /**
     * @param requestOptions
     *            the requestOptions to set
     */
    protected final void setRequestOptions(final RequestOptions requestOptions) {
        this.requestOptions = requestOptions;
    }

    /**
     * @param result
     *            the result to set
     */
    public final void setResult(final RequestResult result) {
        this.result = result;
    }

    /**
     * @param connection
     *            the connection to set
     */
    public final void setConnection(final HttpURLConnection connection) {
        this.connection = connection;
    }

    /**
     * @param sendStream
     *            the stream to send to the server
     */
    public void setSendStream(InputStream sendStream) {
        this.sendStream = sendStream;
    }

    /**
     * @param offset
     *            the stream offset to start copying from
     */
    public void setOffset(Long offset) {
        this.offset = offset;
    }

    /**
     * @param length
     *            the length, in bytes, of the stream
     */
    public void setLength(Long length) {
        this.length = length;
    }

    /**
     * @param lockedETag
     *            the locked ETag
     */
    public void setLockedETag(String lockedETag) {
        this.lockedETag = lockedETag;
    }

    /**
     * @param contentMD5
     *            the contentMD5
     */
    public void setContentMD5(String contentMD5) {
        this.contentMD5 = contentMD5;
    }

    /**
     * @param currentDescriptor
     *              the descriptor value
     */
    protected void setCurrentDescriptor(StreamMd5AndLength currentDescriptor) {
        this.currentDescriptor = currentDescriptor;
    }

    /**
     * @param etagLockCondition
     *            the locked ETag condition
     */
    public void setETagLockCondition(AccessCondition etagLockCondition) {
        this.etagLockCondition = etagLockCondition;
    }

    /**
     * @param arePropertiesPopulated
     *            the arePropertiesPopulated value
     */
    public void setArePropertiesPopulated(boolean arePropertiesPopulated) {
        this.arePropertiesPopulated = arePropertiesPopulated;
    }

    /**
     * @param locationMode
     *            the locationMode value
     */
    public void setLocationMode(LocationMode locationMode) {
        this.locationMode = locationMode;
    }

    /**
     * @param requestLocationMode
     *            the requestLocationMode value
     */
    public void setRequestLocationMode(RequestLocationMode requestLocationMode) {
        this.requestLocationMode = requestLocationMode;
    }

    /**
     * @param storageLocation
     *            the storageLocation value
     */
    public void setCurrentLocation(StorageLocation currentLocation) {
        this.currentLocation = currentLocation;
    }

    /**
     * @param storageUri
     *            the storageUri value
     */
    public void setStorageUri(StorageUri storageUri) {
        this.storageUri = storageUri;
    }

    /**
     * @param currentRequestByteCount
     *            the currentRequestByteCount to set
     */
    public void setCurrentRequestByteCount(long currentRequestByteCount) {
        this.currentRequestByteCount = currentRequestByteCount;
    }

    /**
     * Function to apply the location mode to the request.
     */
    public void setRequestLocationMode() {
        // no-op
    }

    /**
     * @param isSent
     *            the isSent value
     */
    protected void setIsSent(boolean isSent) {
        this.isSent = isSent;
    }

    /**
     * Function to construct the request.
     *
     * @param parentObject
     *            Parent object, i.e. CloudBlobContainer for downloadAttributes etc.
     * @param context
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     * @return a HttpURLConnection configured for the operation.
     * @throws Exception
     */
    public abstract HttpURLConnection buildRequest(C client, P parentObject, OperationContext context) throws Exception;

    /**
     * Function to set custom headers.
     *
     * @param connection
     *            HttpURLConnection configured for the operation.
     * @param parentObject
     *            Parent object, i.e. CloudBlobContainer for downloadAttributes etc.
     * @param context
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     */
    public void setHeaders(HttpURLConnection connection, P parentObject, OperationContext context) {
        // no-op
    }

    /**
     * Function to Sign headers.
     *
     * @param connection
     *            HttpURLConnection configured for the operation.
     * @param client
     *            The service client.
     * @param context
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     * @throws Exception
     */
    public abstract void signRequest(HttpURLConnection connection, C client, OperationContext context) throws Exception;

    /**
     * Pre-Stream Retrieval function.
     *
     * @param command
     *            StorageCommand for the operation.
     * @param parentObject
     *            Parent object, i.e. CloudBlobContainer for downloadAttributes etc.
     * @param client
     *            The service client.
     * @return an Object of the expected result's type.
     * @throws Exception
     */
    public abstract R preProcessResponse(P parentObject, C client, OperationContext context) throws Exception;

    /**
     * Post-Stream Retrieval function.
     *
     * @param connection
     *            HttpURLConnection configured for the operation.
     * @param storageObject
     *            An object of the expected result's type.
     * @return the expected result of the operation.
     * @throws Exception
     */
    public R postProcessResponse(HttpURLConnection connection, P parentObject, C client, OperationContext context,
            R storageObject) throws Exception {
        return storageObject;
    }

    /**
     * Validate the written stream length when length is provided.
     *
     * @throws StorageException
     */
    public void validateStreamWrite(StreamMd5AndLength descriptor) throws StorageException {
        // no-op
    }

    /**
     * Recovery action for retries.
     *
     * @throws IOException
     */
    public void recoveryAction(OperationContext context) throws IOException {
        // no-op
    }

    /**
     * Returns extended error information for this request.
     *
     * @return A {@link StorageExtendedErrorInformation} object that represents the error details for the specified
     *         request.
     */
    public StorageExtendedErrorInformation parseErrorDetails() {
        try {
            if (this.getConnection() == null || this.getConnection().getErrorStream() == null) {
                return null;
            }

            return StorageErrorHandler.getExtendedErrorInformation(this.getConnection().getErrorStream());
        } catch (final Exception e) {
            return null;
        }
    }
}


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

package com.microsoft.azure.storage.table;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.UUID;

import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.StorageErrorCodeStrings;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.StorageExtendedErrorInformation;
import com.microsoft.azure.storage.core.ExecutionEngine;
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.Utility;

/**
 * A class which represents a batch operation. A batch operation is a collection of table operations which are executed
 * by the Storage Service REST API as a single atomic operation, by invoking an Entity Group Transaction.
 *

 * A batch operation may contain up to 100 individual table operations, with the requirement that each operation entity
 * must have same partition key. A batch with a retrieve operation cannot contain any other operations. Note that the
 * total payload of a batch operation is limited to 4MB.
 */
public class TableBatchOperation extends ArrayList<TableOperation> {

    private static final long serialVersionUID = -1192644463287355790L;
    private boolean hasQuery = false;
    private String partitionKey = null;
    private boolean containsWrites = false;

    /**
     * Adds the table operation at the specified index in the batch operation ArrayList.
     *
     * @param index
     *            An int which represents the index in the batch operation ArrayList to add
     *            the table operation at.
     * @param element
     *            The {@link TableOperation} to add to the batch operation.
     */
    @Override
    public void add(final int index, final TableOperation element) {
        Utility.assertNotNull("element", element);

        this.checkSingleQueryPerBatch(element, this.size());

        if (element.getOperationType() == TableOperationType.RETRIEVE) {
            this.lockToPartitionKey(((QueryTableOperation) element).getPartitionKey());
        }
        else {
            this.lockToPartitionKey(element.getEntity().getPartitionKey());
        }

        super.add(index, element);
    }

    /**
     * Adds the table operation to the batch operation ArrayList.
     *
     * @param element
     *            The {@link TableOperation} to add to the batch operation.
     * @return
     *         true if the operation was added successfully.
     */
    @Override
    public boolean add(final TableOperation element) {
        Utility.assertNotNull("element", element);

        this.checkSingleQueryPerBatch(element, this.size());

        if (element.getOperationType() == TableOperationType.RETRIEVE) {
            // Query operation
            this.lockToPartitionKey(((QueryTableOperation) element).getPartitionKey());
        }
        else {
            this.lockToPartitionKey(element.getEntity().getPartitionKey());
        }

        return super.add(element);
    }

    /**
     * Adds the collection of table operations to the batch operation ArrayList starting at the specified
     * index.
     *
     * @param index
     *            An int which represents the index in the batch operation ArrayList to add
     *            the table operation at.
     * @param c
     *            A java.util.Collection of {@link TableOperation} objects to add to the batch operation.
     * @return
     *         true if the operations were added successfully.
     */
    @Override
    public boolean addAll(final int index, final java.util.Collection<? extends TableOperation> c) {
        int size = this.size();
        for (final TableOperation operation : c) {
            Utility.assertNotNull("operation", operation);
            this.checkSingleQueryPerBatch(operation, size);

            if (operation.getEntity() == null) {
                // Query operation
                this.lockToPartitionKey(((QueryTableOperation) operation).getPartitionKey());
            }
            else {
                this.lockToPartitionKey(operation.getEntity().getPartitionKey());
            }

            size++;
        }

        return super.addAll(index, c);
    }

    /**
     * Adds the collection of table operations to the batch operation ArrayList.
     *
     * @param c
     *            A java.util.Collection of {@link TableOperation} objects to add to the batch operation.
     * @return
     *         true if the operations were added successfully.
     */
    @Override
    public boolean addAll(final java.util.Collection<? extends TableOperation> c) {
        int size = this.size();
        for (final TableOperation operation : c) {
            Utility.assertNotNull("operation", operation);
            this.checkSingleQueryPerBatch(operation, size);

            if (operation.getEntity() == null) {
                // Query operation
                this.lockToPartitionKey(((QueryTableOperation) operation).getPartitionKey());
            }
            else {
                this.lockToPartitionKey(operation.getEntity().getPartitionKey());
            }

            size++;
        }

        return super.addAll(c);
    }

    /**
     * Clears all table operations from the batch operation.
     */
    @Override
    public void clear() {
        super.clear();
        checkResetEntityLocks();
    }

    /**
     * Adds a table operation to delete the specified entity to the batch operation.
     *
     * @param entity
     *            The {@link TableEntity} to delete.
     */
    public void delete(final TableEntity entity) {
        this.lockToPartitionKey(entity.getPartitionKey());
        this.add(TableOperation.delete(entity));
    }

    /**
     * Adds a table operation to insert the specified entity to the batch operation.
     *
     * @param entity
     *            The {@link TableEntity} to insert.
     */
    public void insert(final TableEntity entity) {
        insert(entity, false);
    }

    /**
     * Adds a table operation to insert the specified entity to the batch operation.
     *
     * @param entity
     *            The {@link TableEntity} to insert.
     * @param echoContent
     *            The boolean representing whether the message payload should be returned in the response.
     */
    public void insert(final TableEntity entity, boolean echoContent) {
        this.lockToPartitionKey(entity.getPartitionKey());
        this.add(TableOperation.insert(entity, echoContent));
    }

    /**
     * Adds a table operation to insert or merge the specified entity to the batch operation.
     *
     * @param entity
     *            The {@link TableEntity} to insert if not found or to merge if it exists.
     */
    public void insertOrMerge(final TableEntity entity) {
        this.lockToPartitionKey(entity.getPartitionKey());
        this.add(TableOperation.insertOrMerge(entity));
    }

    /**
     * Adds a table operation to insert or replace the specified entity to the batch operation.
     *
     * @param entity
     *            The {@link TableEntity} to insert if not found or to replace if it exists.
     */
    public void insertOrReplace(final TableEntity entity) {
        this.lockToPartitionKey(entity.getPartitionKey());
        this.add(TableOperation.insertOrReplace(entity));
    }

    /**
     * Adds a table operation to merge the specified entity to the batch operation.
     *
     * @param entity
     *            The {@link TableEntity} to merge.
     */
    public void merge(final TableEntity entity) {
        this.lockToPartitionKey(entity.getPartitionKey());
        this.add(TableOperation.merge(entity));
    }

    /**
     * Adds a table operation to retrieve an entity of the specified class type with the specified PartitionKey and
     * RowKey to the batch operation.
     *
     * @param partitionKey
     *            A String containing the PartitionKey of the entity to retrieve.
     * @param rowKey
     *            A String containing the RowKey of the entity to retrieve.
     * @param clazzType
     *            The class of the {@link TableEntity} type for the entity to retrieve.
     */
    public void retrieve(final String partitionKey, final String rowKey, final Class<? extends TableEntity> clazzType) {
        this.lockToPartitionKey(partitionKey);
        this.add(TableOperation.retrieve(partitionKey, rowKey, clazzType));
    }

    /**
     * Adds a table operation to retrieve an entity of the specified class type with the specified PartitionKey and
     * RowKey to the batch operation.
     *
     * @param partitionKey
     *            A String containing the PartitionKey of the entity to retrieve.
     * @param rowKey
     *            A String containing the RowKey of the entity to retrieve.
     * @param resolver
     *            The {@link EntityResolver} implementation to project the entity to retrieve as a particular type in
     *            the result.
     */
    public void retrieve(final String partitionKey, final String rowKey, final EntityResolver<?> resolver) {
        this.lockToPartitionKey(partitionKey);
        this.add(TableOperation.retrieve(partitionKey, rowKey, resolver));
    }

    /**
     * Removes the table operation at the specified index from the batch operation.
     *
     * @param index
     *            An int which represents the index in the ArrayList of the table operation to
     *            remove from the batch operation.
     */
    @Override
    public TableOperation remove(int index) {
        TableOperation op = super.remove(index);
        checkResetEntityLocks();
        return op;
    }

    /**
     * Removes the specified Object from the batch operation.
     *
     * @param o
     *            The Object to remove from the batch operation.
     * @return
     *         true if the object was removed successfully.
     */
    @Override
    public boolean remove(Object o) {
        boolean ret = super.remove(o);
        checkResetEntityLocks();
        return ret;
    }

    /**
     * Removes all elements of the specified collection from the batch operation.
     *
     * @param c
     *            The collection of elements to remove from the batch operation.
     * @return
     *         true if the objects in the collection were removed successfully.
     */
    @Override
    public boolean removeAll(java.util.Collection<?> c) {
        boolean ret = super.removeAll(c);
        checkResetEntityLocks();
        return ret;
    }

    /**
     * Adds a table operation to replace the specified entity to the batch operation.
     *
     * @param entity
     *            The {@link TableEntity} to replace.
     */
    public void replace(final TableEntity entity) {
        this.lockToPartitionKey(entity.getPartitionKey());
        this.add(TableOperation.replace(entity));
    }

    /**
     * Reserved for internal use. Clears internal fields when the batch operation is empty.
     */
    private void checkResetEntityLocks() {
        if (this.size() == 0) {
            this.partitionKey = null;
            this.hasQuery = false;
            this.containsWrites = false;
        }
    }

    /**
     * Reserved for internal use. Verifies that the batch operation either contains no retrieve operations, or contains
     * only a single retrieve operation.
     *
     * @param op
     *            The {@link TableOperation} to be added if the verification succeeds.
     */
    private void checkSingleQueryPerBatch(final TableOperation op, final int size) {
        // if this has a query then no other operations can be added.
        if (this.hasQuery) {
            throw new IllegalArgumentException(SR.RETRIEVE_MUST_BE_ONLY_OPERATION_IN_BATCH);
        }

        if (op.getOperationType() == TableOperationType.RETRIEVE) {
            if (size > 0) {
                throw new IllegalArgumentException(SR.RETRIEVE_MUST_BE_ONLY_OPERATION_IN_BATCH);
            }
            else {
                this.hasQuery = true;
            }
        }

        this.containsWrites = op.getOperationType() != TableOperationType.RETRIEVE;
    }

    /**
     * Reserved for internal use. Verifies that the specified PartitionKey value matches the value in the batch
     * operation.
     *
     * @param partitionKey
     *            The String containing the PartitionKey value to check.
     */
    private void lockToPartitionKey(final String partitionKey) {
        if (this.partitionKey == null) {
            this.partitionKey = partitionKey;
        }
        else {
            if (partitionKey.length() != partitionKey.length() || !this.partitionKey.equals(partitionKey)) {
                throw new IllegalArgumentException(SR.OPS_IN_BATCH_MUST_HAVE_SAME_PARTITION_KEY);
            }
        }
    }

    /**
     * Reserved for internal use. Executes this batch operation on the specified table, using the specified
     * {@link TableRequestOptions} and {@link OperationContext}.
     *

     * This method will invoke the Storage Service REST API to execute this batch operation, using the Table service
     * endpoint and storage account credentials in the {@link CloudTableClient} object.
     *
     * @param client
     *            A {@link CloudTableClient} instance specifying the Table service endpoint and storage account
     *            credentials to use.
     * @param tableName
     *            A String containing the name of the table.
     * @param options
     *            A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout
     *            settings for the operation.
     * @param opContext
     *            An {@link OperationContext} object for tracking the current operation.
     *
     * @return
     *         An ArrayList of {@link TableResult} containing the results of executing the operation.
     *
     * @throws StorageException
     *             if an error occurs in the storage operation.
     */
    protected ArrayList<TableResult> execute(final CloudTableClient client, final String tableName,
            final TableRequestOptions options, final OperationContext opContext) throws StorageException {

        Utility.assertNotNullOrEmpty(TableConstants.TABLE_NAME, tableName);

        if (this.size() == 0) {
            throw new IllegalArgumentException(SR.EMPTY_BATCH_NOT_ALLOWED);
        }

        return ExecutionEngine.executeWithRetry(client, this, this.executeImpl(client, tableName, options, opContext),
                options.getRetryPolicyFactory(), opContext);
    }

    private StorageRequest<CloudTableClient, TableBatchOperation, ArrayList<TableResult>> executeImpl(
            final CloudTableClient client, final String tableName, final TableRequestOptions options,
            final OperationContext opContext) throws StorageException {
        final String batchID = String.format("batch_%s", UUID.randomUUID().toString());
        final String changeSet = String.format("changeset_%s", UUID.randomUUID().toString());
        ByteArrayOutputStream sendStream = new ByteArrayOutputStream();
        try {
            MimeHelper.writeBatchToStream(sendStream, options, tableName, client.getTransformedEndPoint(opContext)
                    .getPrimaryUri(), this, batchID, changeSet, opContext);
            final byte[] batchBytes = sendStream.toByteArray();
            final StorageRequest<CloudTableClient, TableBatchOperation, ArrayList<TableResult>> batchRequest = new StorageRequest<CloudTableClient, TableBatchOperation, ArrayList<TableResult>>(
                    options, client.getStorageUri()) {

                @Override
                public void setRequestLocationMode() {
                    this.setRequestLocationMode(TableBatchOperation.this.containsWrites ? RequestLocationMode.PRIMARY_ONLY
                            : RequestLocationMode.PRIMARY_OR_SECONDARY);
                }

                @Override
                public HttpURLConnection buildRequest(CloudTableClient client, TableBatchOperation batch,
                        OperationContext context) throws Exception {

                    this.setSendStream(new ByteArrayInputStream(batchBytes));
                    this.setLength((long) batchBytes.length);
                    return TableRequest.batch(client.getTransformedEndPoint(context).getUri(this.getCurrentLocation()),
                            options, null, context, batchID);
                }

                @Override
                public void signRequest(HttpURLConnection connection, CloudTableClient client, OperationContext context)
                        throws Exception {
                    StorageRequest.signTableRequest(connection, client, -1L, context);
                }

                @Override
                public ArrayList<TableResult> preProcessResponse(TableBatchOperation batch, CloudTableClient client,
                        OperationContext context) throws Exception {
                    if (this.getResult().getStatusCode() != HttpURLConnection.HTTP_ACCEPTED) {
                        this.setNonExceptionedRetryableFailure(true);
                    }

                    return null;
                }

                @Override
                public ArrayList<TableResult> postProcessResponse(HttpURLConnection connection,
                        TableBatchOperation batch, CloudTableClient client, OperationContext context,
                        ArrayList<TableResult> storageObject) throws Exception {
                    final InputStream streamRef = connection.getInputStream();
                    ArrayList<MimePart> responseParts = null;

                    final String contentType = connection.getHeaderField(Constants.HeaderConstants.CONTENT_TYPE);

                    final String[] headerVals = contentType.split("multipart/mixed; boundary=");
                    if (headerVals == null || headerVals.length != 2) {
                        throw new StorageException(StorageErrorCodeStrings.OUT_OF_RANGE_INPUT, SR.INVALID_CONTENT_TYPE,
                                Constants.HeaderConstants.HTTP_UNUSED_306, null /* extendedErrorInfo */, null /* innerException */);
                    }

                    responseParts = MimeHelper.readBatchResponseStream(streamRef, headerVals[1], opContext,
                            options.getTablePayloadFormat());

                    final ArrayList<TableResult> result = new ArrayList<TableResult>();
                    for (int m = 0; m < batch.size(); m++) {
                        final TableOperation currOp = batch.get(m);
                        final MimePart currMimePart = responseParts.get(m);

                        boolean failFlag = false;

                        // Validate response
                        if (currOp.getOperationType() == TableOperationType.INSERT) {
                            if (currOp.getEchoContent()
                                    && currMimePart.httpStatusCode != HttpURLConnection.HTTP_CREATED) {
                                // Insert should receive created if echo content is on
                                failFlag = true;
                            }
                            else if (!currOp.getEchoContent()
                                    && currMimePart.httpStatusCode != HttpURLConnection.HTTP_NO_CONTENT) {
                                // Insert should receive no content if echo content is off
                                failFlag = true;
                            }
                        }
                        else if (currOp.getOperationType() == TableOperationType.RETRIEVE) {
                            if (currMimePart.httpStatusCode == HttpURLConnection.HTTP_NOT_FOUND) {
                                // Empty result
                                result.add(new TableResult(currMimePart.httpStatusCode));
                                return result;
                            }

                            // Point query should receive ok.
                            if (currMimePart.httpStatusCode != HttpURLConnection.HTTP_OK) {
                                failFlag = true;
                            }
                        }
                        else {
                            if (currMimePart.httpStatusCode != HttpURLConnection.HTTP_NO_CONTENT) {
                                // All others should receive no content. (delete, merge, upsert etc)
                                failFlag = true;
                            }
                        }

                        if (failFlag) {
                            throw new TableServiceException(currMimePart.httpStatusCode,
                                    currMimePart.httpStatusMessage, currOp, new StringReader(currMimePart.payload),
                                    options.getTablePayloadFormat());
                        }

                        ByteArrayInputStream byteStream = null;

                        if ((currOp.getOperationType() == TableOperationType.INSERT && currOp.getEchoContent())
                                || currOp.getOperationType() == TableOperationType.RETRIEVE) {
                            byteStream = new ByteArrayInputStream(currMimePart.payload.getBytes());
                        }

                        result.add(currOp.parseResponse(byteStream, currMimePart.httpStatusCode,
                                currMimePart.headers.get(TableConstants.HeaderConstants.ETAG), opContext, options));
                    }

                    return result;
                }

                @Override
                public StorageExtendedErrorInformation parseErrorDetails() {
                    return TableStorageErrorDeserializer.parseErrorDetails(this);
                }
            };

            return batchRequest;
        }
        catch (IOException e) {
            // The request was not even made. There was an error while trying to read the batch contents. Just throw.
            StorageException translatedException = StorageException.translateClientException(e);
            throw translatedException;
        }
        catch (URISyntaxException e) {
            // The request was not even made. There was an error while trying to read the batch contents. Just throw.
            StorageException translatedException = StorageException.translateClientException(e);
            throw translatedException;
        }
    }

    /**
     * Reserved for internal use. Removes all the table operations at indexes in the specified range from the batch
     * operation ArrayList.
     *
     * @param fromIndex
     *            An int which represents the inclusive lower bound of the range of {@link TableOperation}
     *            objects to remove from the batch operation ArrayList.
     * @param toIndex
     *            An int which represents the exclusive upper bound of the range of {@link TableOperation}
     *            objects to remove from the batch operation ArrayList.
     */
    @Override
    protected void removeRange(int fromIndex, int toIndex) {
        super.removeRange(fromIndex, toIndex);
        checkResetEntityLocks();
    }
}


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

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

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

/**
 * Represents storage credentials for delegated access to Blob service resources via a shared access signature.
 */
public final class StorageCredentialsSharedAccessSignature extends StorageCredentials {

    /**
     * Stores the shared access signature token.
     */
    private final String token;

    /**
     * Creates an instance of the StorageCredentialsSharedAccessSignature class using the specified shared
     * access signature token.
     *
     * @param token
     *            A String that represents shared access signature token.
     */
    public StorageCredentialsSharedAccessSignature(final String token) {
        this.token = token;

        if (token == null) {
            this.setHttpsOnly(false);
        }
        else {
            try {
                Map<String, String[]> queryParams = PathUtility.parseQueryString(token);
                final String[] protocols = queryParams.get(Constants.QueryConstants.SIGNED_PROTOCOLS);
                this.setHttpsOnly((protocols != null) && Constants.HTTPS.equals(protocols[0]));
            }
            catch (StorageException e) {
                this.setHttpsOnly(false);
            }
        }
    }

    /**
     * Returns the shared access signature token.
     *
     * @return A String that contains the token.
     */
    public String getToken() {
        return this.token;
    }

    /**
     * Returns a String that represents this instance, optionally including sensitive data.
     *
     * @param exportSecrets
     *            true to include sensitive data in the return string; otherwise, false.
     * @return A String that represents this object, optionally including sensitive data.
     */
    @Override
    public String toString(final boolean exportSecrets) {
        return String.format("%s=%s", CloudStorageAccount.SHARED_ACCESS_SIGNATURE_NAME, exportSecrets ? this.token
                : "[signature hidden]");
    }

    /**
     * Transforms a resource URI into a shared access signature URI, by appending a shared access token and using the
     * specified operation context.
     *
     * @param resourceUri
     *            A java.net.URI object that represents the resource URI to be transformed.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     *
     * @return A java.net.URI object that represents the signature, including the resource URI and the
     *         shared access token.
     *
     * @throws StorageException
     *             If a storage service error occurred.
     * @throws URISyntaxException
     *             If the resource URI is not properly formatted.
     */
    @Override
    public URI transformUri(final URI resourceUri, final OperationContext opContext)
            throws URISyntaxException, StorageException {
        if (resourceUri == null) {
            return null;
        }

        if(this.isHttpsOnly() && !resourceUri.getScheme().equals(Constants.HTTPS)) {
            throw new IllegalArgumentException(SR.CANNOT_TRANSFORM_NON_HTTPS_URI_WITH_HTTPS_ONLY_CREDENTIALS);
        }

        // append the sas token to the resource uri
        URI sasUri = PathUtility.addToQuery(resourceUri, this.token);

        // append the api version parameter to the sas uri
        String apiVersion = Constants.QueryConstants.API_VERSION + "=" + Constants.HeaderConstants.TARGET_STORAGE_VERSION;
        return PathUtility.addToQuery(sasUri, apiVersion);
    }

    /**
     * Transforms a resource URI into a shared access signature URI, by appending a shared access token and using the
     * specified operation context.
     *
     * @param resourceUri
     *            A StorageUri object that represents the resource URI to be transformed.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     *
     * @return A StorageUri object that represents the signature, including the resource URI and the
     *         shared access token.
     *
     * @throws StorageException
     *             If a storage service error occurred.
     * @throws URISyntaxException
     *             If the resource URI is not properly formatted.
     */
    @Override
    public StorageUri transformUri(StorageUri resourceUri, OperationContext opContext) throws URISyntaxException,
            StorageException {
        return new StorageUri(this.transformUri(resourceUri.getPrimaryUri(), opContext), this.transformUri(
                resourceUri.getSecondaryUri(), opContext));
    }
}


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

package com.microsoft.azure.storage.table;

import java.net.HttpURLConnection;

import com.microsoft.azure.storage.ResultContinuation;
import com.microsoft.azure.storage.ResultContinuationType;

/**
 * Reserved for internal use. A class used to help parse responses from the Table service.
 */
class TableResponse {
    /**
     * Reserved for internal use. A static factory method that constructs a {@link ResultContinuation} instance from the
     * continuation token information in a table operation response, if any.
     *
     * @param queryRequest
     *            The java.net.HttpURLConnection request response to parse for continuation token
     *            information.
     *
     * @return
     *         A {@link ResultContinuation} instance from continuation token information in the response, or
     *         null if none is found.
     */
    protected static ResultContinuation getTableContinuationFromResponse(final HttpURLConnection queryRequest) {
        final ResultContinuation retVal = new ResultContinuation();
        retVal.setContinuationType(ResultContinuationType.TABLE);

        boolean foundToken = false;

        String tString = queryRequest.getHeaderField(TableConstants.TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION
                .concat(TableConstants.TABLE_SERVICE_NEXT_PARTITION_KEY));
        if (tString != null) {
            retVal.setNextPartitionKey(tString);
            foundToken = true;
        }

        tString = queryRequest.getHeaderField(TableConstants.TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION
                .concat(TableConstants.TABLE_SERVICE_NEXT_ROW_KEY));
        if (tString != null) {
            retVal.setNextRowKey(tString);
            foundToken = true;
        }

        tString = queryRequest.getHeaderField(TableConstants.TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION
                .concat(TableConstants.TABLE_SERVICE_NEXT_MARKER));
        if (tString != null) {
            retVal.setNextMarker(tString);
            foundToken = true;
        }

        tString = queryRequest.getHeaderField(TableConstants.TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION
                .concat(TableConstants.TABLE_SERVICE_NEXT_TABLE_NAME));
        if (tString != null) {
            retVal.setNextTableName(tString);
            foundToken = true;
        }

        return foundToken ? retVal : null;
    }
}