com.microsoft.windowsazure.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.windowsazure.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 javax.xml.stream.XMLStreamException;
import com.fasterxml.jackson.core.JsonParseException;
import com.microsoft.windowsazure.storage.Constants;
import com.microsoft.windowsazure.storage.OperationContext;
import com.microsoft.windowsazure.storage.StorageErrorCodeStrings;
import com.microsoft.windowsazure.storage.StorageException;
import com.microsoft.windowsazure.storage.core.PathUtility;
import com.microsoft.windowsazure.storage.core.SR;
import com.microsoft.windowsazure.storage.core.UriQueryBuilder;
import com.microsoft.windowsazure.storage.core.Utility;
/**
* Reserved for internal use. A class used to read and write MIME requests and responses.
*/
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.
* @throws JsonParseException
* if an error occurs while parsing the stream.
*/
protected static ArrayList readBatchResponseStream(final InputStream inStream,
final String expectedBundaryName, final OperationContext opContext, TablePayloadFormat format)
throws IOException, StorageException, XMLStreamException {
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.
* @throws XMLStreamException
* if an error occurs accessing the stream.
*/
protected 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, XMLStreamException {
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
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,
generateContentTypeHeaderValue(options.getTablePayloadFormat()));
mimePart.payload = writeStringForOperation(op, options.getTablePayloadFormat(), 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
* AtomPub format.
*
* @param operation
* A {@link TableOperation} containing the entity to write to the returned String
.
* @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 AtomPub format
* @throws StorageException
* if a Storage error occurs.
* @throws XMLStreamException
* if an error occurs creating or writing to the output string.
* @throws IOException
*/
private static String writeStringForOperation(final TableOperation operation, TablePayloadFormat format,
final OperationContext opContext) throws StorageException, XMLStreamException, IOException {
Utility.assertNotNull("entity", operation.getEntity());
final StringWriter outWriter = new StringWriter();
TableParser.writeSingleEntityToString(outWriter, format, operation.getEntity(), false, opContext);
outWriter.write("\r\n");
return outWriter.toString();
}
private static String generateAcceptHeaderValue(TablePayloadFormat payloadFormat) {
if (payloadFormat == TablePayloadFormat.AtomPub) {
return TableConstants.HeaderConstants.ATOM_ACCEPT_TYPE;
}
else 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;
}
}
private static String generateContentTypeHeaderValue(TablePayloadFormat payloadFormat) {
if (payloadFormat == TablePayloadFormat.AtomPub) {
return TableConstants.HeaderConstants.ATOM_CONTENT_TYPE;
}
else {
return TableConstants.HeaderConstants.JSON_CONTENT_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 - 2025 Weber Informatics LLC | Privacy Policy