All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.azure.storage.table.MimeHelper Maven / Gradle / Ivy

/**
 * 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.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;

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

/**
 * Reserved for internal use. A class used to read and write MIME requests and responses.
 */
final class MimeHelper {
    /**
     * Reserved for internal use. Reads the response stream from a batch operation into an ArrayList of
     * {@link MimePart} objects.
     * 
     * @param inStream
     *            An {@link InputStream} containing the operation response stream.
     * @param expectedBundaryName
     *            A String containing the MIME part boundary string.
     * @param opContext
     *            An {@link OperationContext} object for tracking the current operation. Specify null to
     *            safely ignore operation context.
     * @param format
     *            The {@link TablePayloadFormat} that will be used for parsing
     * @return
     *         An ArrayList of {@link MimePart} objects parsed from the input stream.
     * @throws StorageException
     *             if a storage service error occurs.
     * @throws IOException
     *             if an error occurs while accessing the stream with Json.
     */
    static ArrayList readBatchResponseStream(final InputStream inStream, final String expectedBundaryName,
            final OperationContext opContext, TablePayloadFormat format) throws IOException, StorageException {
        final ArrayList result = new ArrayList();
        final InputStreamReader streamReader = new InputStreamReader(inStream, Constants.UTF8_CHARSET);
        final BufferedReader reader = new BufferedReader(streamReader);
        final String mungedExpectedBoundaryName = "--".concat(expectedBundaryName);

        final MimeHeader docHeader = readMimeHeader(reader, opContext);
        if (docHeader.boundary == null || !docHeader.boundary.equals(mungedExpectedBoundaryName)) {
            throw generateMimeParseException();
        }

        MimeHeader currHeader = null;

        // No explicit changeset present
        if (docHeader.subBoundary == null) {
            do {
                result.add(readMimePart(reader, docHeader.boundary, opContext));
                currHeader = readMimeHeader(reader, opContext);
            } while (currHeader != null);
        }
        else {
            // explicit changeset present.
            currHeader = readMimeHeader(reader, opContext);
            if (currHeader == null) {
                throw generateMimeParseException();
            }
            else {
                do {
                    result.add(readMimePart(reader, docHeader.subBoundary, opContext));
                    currHeader = readMimeHeader(reader, opContext);
                } while (currHeader != null);
            }
        }

        return result;
    }

    /**
     * Reserved for internal use. Writes the batch operation to the output stream using batch request syntax.
     * Batch request syntax is described in the MSDN topic Performing Entity Group
     * Transactions.
     * 
     * @param outStream
     *            The {@link OutputStream} to write the batch request to.
     * @param tableName
     *            A String containing the name of the table to apply each operation to.
     * @param batch
     *            A {@link TableBatchOperation} containing the operations to write to the output stream
     * @param batchID
     *            A String containing the identifier to use as the MIME boundary for the batch request.
     * @param changeSet
     *            A String containing the identifier to use as the MIME boundary for operations within the
     *            batch.
     * @param opContext
     *            An {@link OperationContext} object for tracking the current operation. Specify null to
     *            safely ignore operation context.
     * @throws IOException
     *             if an IO error occurs.
     * @throws URISyntaxException
     *             if an invalid URI is used.
     * @throws StorageException
     *             if an error occurs accessing the Storage service.
     */
    static void writeBatchToStream(final OutputStream outStream, final TableRequestOptions options,
            final String tableName, final URI baseUri, final TableBatchOperation batch, final String batchID,
            final String changeSet, final OperationContext opContext) throws IOException, URISyntaxException,
            StorageException {
        final OutputStreamWriter outWriter = new OutputStreamWriter(outStream, Constants.UTF8_CHARSET);

        MimePart mimePart;
        int contentID = 0;
        boolean isQuery = batch.size() == 1 && batch.get(0).getOperationType() == TableOperationType.RETRIEVE;

        // when batch is made, a check is done to make sure only one retrieve is added
        if (isQuery) {
            final QueryTableOperation qOp = (QueryTableOperation) batch.get(0);
            // Write MIME batch Header
            MimeHelper.writeMIMEBoundary(outWriter, batchID);

            mimePart = new MimePart();
            mimePart.op = qOp.getOperationType();

            UriQueryBuilder builder = new UriQueryBuilder();
            mimePart.requestIdentity = builder.addToURI(PathUtility.appendPathToSingleUri(baseUri,
                    qOp.generateRequestIdentityWithTable(tableName)));

            mimePart.headers.put(Constants.HeaderConstants.ACCEPT,
                    generateAcceptHeaderValue(options.getTablePayloadFormat()));
            mimePart.headers.put(TableConstants.HeaderConstants.MAX_DATA_SERVICE_VERSION,
                    TableConstants.HeaderConstants.MAX_DATA_SERVICE_VERSION_VALUE);

            outWriter.write(mimePart.toRequestString());
        }
        else {
            // Write MIME batch Header
            MimeHelper.writeMIMEBoundary(outWriter, batchID);
            MimeHelper.writeMIMEContentType(outWriter, changeSet);
            outWriter.write("\r\n");

            // Write each operation
            for (final TableOperation op : batch) {

                // New mime part for changeset
                MimeHelper.writeMIMEBoundary(outWriter, changeSet);

                mimePart = new MimePart();
                mimePart.op = op.getOperationType();

                UriQueryBuilder builder = new UriQueryBuilder();
                mimePart.requestIdentity = builder.addToURI(PathUtility.appendPathToSingleUri(baseUri,
                        op.generateRequestIdentityWithTable(tableName)));

                mimePart.headers.put(TableConstants.HeaderConstants.CONTENT_ID, Integer.toString(contentID));
                mimePart.headers.put(Constants.HeaderConstants.ACCEPT,
                        generateAcceptHeaderValue(options.getTablePayloadFormat()));
                mimePart.headers.put(TableConstants.HeaderConstants.MAX_DATA_SERVICE_VERSION,
                        TableConstants.HeaderConstants.MAX_DATA_SERVICE_VERSION_VALUE);

                if (op.getOperationType() == TableOperationType.INSERT_OR_MERGE
                        || op.getOperationType() == TableOperationType.MERGE) {
                    // post tunnelling
                    options.assertNoEncryptionPolicyOrStrictMode();
                    
                    mimePart.headers.put(TableConstants.HeaderConstants.X_HTTP_METHOD,
                            TableOperationType.MERGE.toString());
                }

                // etag
                if (op.getOperationType() == TableOperationType.DELETE
                        || op.getOperationType() == TableOperationType.REPLACE
                        || op.getOperationType() == TableOperationType.MERGE) {
                    if (op.getEntity() != null && op.getEntity().getEtag() != null) {
                        mimePart.headers.put(Constants.HeaderConstants.IF_MATCH, op.getEntity().getEtag());
                    }
                }

                // prefer header
                if (op.getOperationType() == TableOperationType.INSERT) {
                    mimePart.headers.put(TableConstants.HeaderConstants.PREFER,
                            op.getEchoContent() ? TableConstants.HeaderConstants.RETURN_CONTENT
                                    : TableConstants.HeaderConstants.RETURN_NO_CONTENT);
                }

                if (op.getOperationType() != TableOperationType.DELETE) {
                    mimePart.headers.put(Constants.HeaderConstants.CONTENT_TYPE, TableConstants.HeaderConstants.JSON_CONTENT_TYPE);

                    mimePart.payload = writeStringForOperation(op, options, opContext);
                    mimePart.headers.put(Constants.HeaderConstants.CONTENT_LENGTH,
                            Integer.toString(mimePart.payload.getBytes(Constants.UTF8_CHARSET).length));
                }

                // write the request (no body)
                outWriter.write(mimePart.toRequestString());

                contentID = contentID + 1;
            }
        }

        if (!isQuery) {
            // end changeset
            MimeHelper.writeMIMEBoundaryClosure(outWriter, changeSet);
        }
        // end batch
        MimeHelper.writeMIMEBoundaryClosure(outWriter, batchID);

        outWriter.flush();
    }

    /**
     * Reserved for internal use. A static factory method that constructs a {@link MimeHeader} by parsing the MIME
     * header
     * data from a {@link BufferedReader}.
     * 
     * @param reader
     *            The {@link BufferedReader} containing the response stream to parse.
     * @param opContext
     *            An {@link OperationContext} object for tracking the current operation. Specify null to
     *            safely ignore operation context.
     * @return
     *         A {@link MimeHeader} constructed by parsing the MIME header data from the {@link BufferedReader}.
     * @throws IOException
     *             if an error occurs accessing the input stream.
     * @throws StorageException
     *             if an error occurs parsing the input stream.
     */
    private static MimeHeader readMimeHeader(final BufferedReader reader, final OperationContext opContext)
            throws IOException, StorageException {
        final MimeHeader retHeader = new MimeHeader();
        reader.mark(1024 * 1024);

        // First thing is separator
        retHeader.boundary = getNextLineSkippingBlankLines(reader);
        if (retHeader.boundary.endsWith("--")) {
            return null;
        }
        if (!retHeader.boundary.startsWith("--")) {
            reader.reset();
            return null;
        }

        for (int m = 0; m < 2; m++) {
            final String tempString = reader.readLine();
            if (tempString == null || tempString.length() == 0) {
                break;
            }

            if (tempString.startsWith("Content-Type:")) {
                final String[] headerVals = tempString.split("Content-Type: ");
                if (headerVals == null || headerVals.length != 2) {
                    throw generateMimeParseException();
                }
                retHeader.contentType = headerVals[1];
            }
            else if (tempString.startsWith("Content-Transfer-Encoding:")) {
                final String[] headerVals = tempString.split("Content-Transfer-Encoding: ");
                if (headerVals == null || headerVals.length != 2) {
                    throw generateMimeParseException();
                }
                retHeader.contentTransferEncoding = headerVals[1];
            }
            else {
                throw generateMimeParseException();
            }
        }

        // Validate headers
        if (Utility.isNullOrEmpty(retHeader.boundary) || retHeader.contentType == null) {
            throw generateMimeParseException();
        }

        if (retHeader.contentType.startsWith("multipart/mixed; boundary=")) {
            final String[] headerVals = retHeader.contentType.split("multipart/mixed; boundary=");
            if (headerVals == null || headerVals.length != 2) {
                throw generateMimeParseException();
            }
            retHeader.subBoundary = "--".concat(headerVals[1]);
        }
        else if (!retHeader.contentType.equals("application/http")) {
            throw generateMimeParseException();
        }

        if (retHeader.contentTransferEncoding != null && !retHeader.contentTransferEncoding.equals("binary")) {
            throw generateMimeParseException();
        }

        return retHeader;
    }

    // Returns at start of next mime boundary header
    /**
     * Reserved for internal use. A static factory method that generates a {@link MimePart} containing the next MIME
     * part read from the {@link BufferedReader}.
     * The {@link BufferedReader} is left positioned at the start of the next MIME boundary header.
     * 
     * @param reader
     *            The {@link BufferedReader} containing the response stream to parse.
     * @param boundary
     *            A String containing the MIME part boundary string.
     *            An {@link OperationContext} object for tracking the current operation. Specify null to
     *            safely ignore operation context.
     * @return
     *         A {@link MimePart} constructed by parsing the next MIME part data from the {@link BufferedReader}.
     * @throws IOException
     *             if an error occured accessing the input stream.
     * @throws StorageException
     *             if an error occured parsing the input stream.
     */
    private static MimePart readMimePart(final BufferedReader reader, final String boundary,
            final OperationContext opContext) throws IOException, StorageException {
        final MimePart retPart = new MimePart();
        // Read HttpStatus code
        String tempStr = getNextLineSkippingBlankLines(reader);
        if (!tempStr.startsWith("HTTP/1.1 ")) {
            throw generateMimeParseException();
        }

        final String[] headerVals = tempStr.split(" ");

        if (headerVals.length < 3) {
            throw generateMimeParseException();
        }

        retPart.httpStatusCode = Integer.parseInt(headerVals[1]);
        // "HTTP/1.1 XXX ".length() => 13
        retPart.httpStatusMessage = tempStr.substring(13);

        // Read headers
        tempStr = reader.readLine();
        while (tempStr != null && tempStr.length() > 0) {
            final String[] headerParts = tempStr.split(": ");
            if (headerParts.length < 2) {
                throw generateMimeParseException();
            }

            retPart.headers.put(headerParts[0], headerParts[1]);
            tempStr = reader.readLine();
        }

        // Store xml payload
        reader.mark(1024 * 1024);
        tempStr = getNextLineSkippingBlankLines(reader);

        if (tempStr == null) {
            throw generateMimeParseException();
        }

        // empty body
        if (tempStr.startsWith(boundary)) {
            reader.reset();
            retPart.payload = Constants.EMPTY_STRING;
            return retPart;
        }
        final StringBuilder payloadBuilder = new StringBuilder();
        // read until mime closure or end of file
        while (!tempStr.startsWith(boundary)) {
            payloadBuilder.append(tempStr);
            reader.mark(1024 * 1024);
            tempStr = getNextLineSkippingBlankLines(reader);
            if (tempStr == null) {
                throw generateMimeParseException();
            }
        }

        // positions stream at start of next MIME Header
        reader.reset();

        retPart.payload = payloadBuilder.toString();

        return retPart;
    }

    /**
     * Reserved for internal use. Writes a MIME part boundary to the output stream.
     * 
     * @param outWriter
     *            The {@link OutputStreamWriter} to write the MIME part boundary to.
     * @param boundaryID
     *            The String containing the MIME part boundary string.
     * @throws IOException
     *             if an error occurs writing to the output stream.
     */
    private static void writeMIMEBoundary(final OutputStreamWriter outWriter, final String boundaryID)
            throws IOException {
        outWriter.write(String.format("--%s\r\n", boundaryID));
    }

    /**
     * Reserved for internal use. Writes a MIME part boundary closure to the output stream.
     * 
     * @param outWriter
     *            The {@link OutputStreamWriter} to write the MIME part boundary closure to.
     * @param boundaryID
     *            The String containing the MIME part boundary string.
     * @throws IOException
     *             if an error occurs writing to the output stream.
     */
    private static void writeMIMEBoundaryClosure(final OutputStreamWriter outWriter, final String boundaryID)
            throws IOException {
        outWriter.write(String.format("--%s--\r\n", boundaryID));
    }

    /**
     * Reserved for internal use. Writes a MIME content type string to the output stream.
     * 
     * @param outWriter
     *            The {@link OutputStreamWriter} to write the MIME content type string to.
     * @param boundaryID
     *            The String containing the MIME part boundary string.
     * @throws IOException
     *             if an error occurs writing to the output stream.
     */
    private static void writeMIMEContentType(final OutputStreamWriter outWriter, final String boundaryName)
            throws IOException {
        outWriter.write(String.format("Content-Type: multipart/mixed; boundary=%s\r\n", boundaryName));
    }

    /**
     * Reserved for internal use. Generates a String containing the entity associated with an operation in
     * Json format.
     * 
     * @param operation
     *            A {@link TableOperation} containing the entity to write to the returned String.
     * @param options
     *            The {@link TableRequestOptions} to use for serializing.
     * @param opContext
     *            An {@link OperationContext} object for tracking the current operation. Specify null to
     *            safely ignore operation context.
     * @return
     *         A String containing the entity associated with the operation in Json format
     * @throws StorageException
     *             if a Storage error occurs.
     * @throws IOException
     */
    private static String writeStringForOperation(final TableOperation operation, final TableRequestOptions options,
            final OperationContext opContext) throws StorageException, IOException {
        Utility.assertNotNull("entity", operation.getEntity());
        final StringWriter outWriter = new StringWriter();

        TableEntitySerializer.writeSingleEntityToString(outWriter, options, operation.getEntity(), false, opContext);
        outWriter.write("\r\n");

        return outWriter.toString();
    }

    private static String generateAcceptHeaderValue(TablePayloadFormat payloadFormat) {
        if (payloadFormat == TablePayloadFormat.JsonFullMetadata) {
            return TableConstants.HeaderConstants.JSON_FULL_METADATA_ACCEPT_TYPE;
        }
        else if (payloadFormat == TablePayloadFormat.Json) {
            return TableConstants.HeaderConstants.JSON_ACCEPT_TYPE;
        }
        else {
            return TableConstants.HeaderConstants.JSON_NO_METADATA_ACCEPT_TYPE;
        }
    }

    /**
     * Reserved for internal use. A static factory method that generates a {@link StorageException} for invalid MIME
     * responses.
     * 
     * @return
     *         The {@link StorageException} for the invalid MIME response.
     */
    private static StorageException generateMimeParseException() {
        return new StorageException(StorageErrorCodeStrings.OUT_OF_RANGE_INPUT, SR.INVALID_MIME_RESPONSE,
                Constants.HeaderConstants.HTTP_UNUSED_306, null, null);
    }

    /**
     * Reserved for internal use. Returns the next non-blank line from the {@link BufferedReader}.
     * 
     * @param reader
     *            The {@link BufferedReader} to read lines from.
     * @return
     *         A String containing the next non-blank line from the {@link BufferedReader}, or
     *         null.
     * @throws IOException
     *             if an error occurs reading from the {@link BufferedReader}.
     */
    private static String getNextLineSkippingBlankLines(final BufferedReader reader) throws IOException {
        String tString = null;
        do {
            tString = reader.readLine();
        } while (tString != null && tString.length() == 0);

        return tString;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy