com.microsoft.azure.storage.table.MimeHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of azure-storage Show documentation
Show all versions of azure-storage Show documentation
SDK for Microsoft Azure Storage Clients
/**
* 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;
}
}