blob: dccac1bbd29c13249dff5d3b48a32e4d5c0b9bf1 [file] [log] [blame]
/// <reference path="odata-utils.js" />
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// odata-batch.js
(function (window, undefined) {
var datajs = window.odatajs || {};
var odata = window.OData || {};
// Imports
var extend = odatajs.extend;
var isArray = odatajs.isArray;
var trimString = odatajs.trimString;
var contentType = odata.contentType;
var handler = odata.handler;
var isBatch = odata.isBatch;
var MAX_DATA_SERVICE_VERSION = odata.MAX_DATA_SERVICE_VERSION;
var normalizeHeaders = odata.normalizeHeaders;
var payloadTypeOf = odata.payloadTypeOf;
var prepareRequest = odata.prepareRequest;
// CONTENT START
var batchMediaType = "multipart/mixed";
var responseStatusRegex = /^HTTP\/1\.\d (\d{3}) (.*)$/i;
var responseHeaderRegex = /^([^()<>@,;:\\"\/[\]?={} \t]+)\s?:\s?(.*)/;
var hex16 = function () {
/// <summary>
/// Calculates a random 16 bit number and returns it in hexadecimal format.
/// </summary>
/// <returns type="String">A 16-bit number in hex format.</returns>
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substr(1);
};
var createBoundary = function (prefix) {
/// <summary>
/// Creates a string that can be used as a multipart request boundary.
/// </summary>
/// <param name="prefix" type="String" optional="true">String to use as the start of the boundary string</param>
/// <returns type="String">Boundary string of the format: <prefix><hex16>-<hex16>-<hex16></returns>
return prefix + hex16() + "-" + hex16() + "-" + hex16();
};
var partHandler = function (context) {
/// <summary>
/// Gets the handler for data serialization of individual requests / responses in a batch.
/// </summary>
/// <param name="context">Context used for data serialization.</param>
/// <returns>Handler object.</returns>
return context.handler.partHandler;
};
var currentBoundary = function (context) {
/// <summary>
/// Gets the current boundary used for parsing the body of a multipart response.
/// </summary>
/// <param name="context">Context used for parsing a multipart response.</param>
/// <returns type="String">Boundary string.</returns>
var boundaries = context.boundaries;
return boundaries[boundaries.length - 1];
};
var batchParser = function (handler, text, context) {
/// <summary>Parses a batch response.</summary>
/// <param name="handler">This handler.</param>
/// <param name="text" type="String">Batch text.</param>
/// <param name="context" type="Object">Object with parsing context.</param>
/// <returns>An object representation of the batch.</returns>
var boundary = context.contentType.properties["boundary"];
return { __batchResponses: readBatch(text, { boundaries: [boundary], handlerContext: context }) };
};
var batchSerializer = function (handler, data, context) {
/// <summary>Serializes a batch object representation into text.</summary>
/// <param name="handler">This handler.</param>
/// <param name="data" type="Object">Representation of a batch.</param>
/// <param name="context" type="Object">Object with parsing context.</param>
/// <returns>An text representation of the batch object; undefined if not applicable.</returns>
var cType = context.contentType = context.contentType || contentType(batchMediaType);
if (cType.mediaType === batchMediaType) {
return writeBatch(data, context);
}
};
var readBatch = function (text, context) {
/// <summary>
/// Parses a multipart/mixed response body from from the position defined by the context.
/// </summary>
/// <param name="text" type="String" optional="false">Body of the multipart/mixed response.</param>
/// <param name="context">Context used for parsing.</param>
/// <returns>Array of objects representing the individual responses.</returns>
var delimiter = "--" + currentBoundary(context);
// Move beyond the delimiter and read the complete batch
readTo(text, context, delimiter);
// Ignore the incoming line
readLine(text, context);
// Read the batch parts
var responses = [];
var partEnd;
while (partEnd !== "--" && context.position < text.length) {
var partHeaders = readHeaders(text, context);
var partContentType = contentType(partHeaders["Content-Type"]);
var changeResponses;
if (partContentType && partContentType.mediaType === batchMediaType) {
context.boundaries.push(partContentType.properties["boundary"]);
try {
changeResponses = readBatch(text, context);
} catch (e) {
e.response = readResponse(text, context, delimiter);
changeResponses = [e];
}
responses.push({ __changeResponses: changeResponses });
context.boundaries.pop();
readTo(text, context, "--" + currentBoundary(context));
} else {
if (!partContentType || partContentType.mediaType !== "application/http") {
throw { message: "invalid MIME part type " };
}
// Skip empty line
readLine(text, context);
// Read the response
var response = readResponse(text, context, delimiter);
try {
if (response.statusCode >= 200 && response.statusCode <= 299) {
partHandler(context.handlerContext).read(response, context.handlerContext);
} else {
// Keep track of failed responses and continue processing the batch.
response = { message: "HTTP request failed", response: response };
}
} catch (e) {
response = e;
}
responses.push(response);
}
partEnd = text.substr(context.position, 2);
// Ignore the incoming line.
readLine(text, context);
}
return responses;
};
var readHeaders = function (text, context) {
/// <summary>
/// Parses the http headers in the text from the position defined by the context.
/// </summary>
/// <param name="text" type="String" optional="false">Text containing an http response's headers</param>
/// <param name="context">Context used for parsing.</param>
/// <returns>Object containing the headers as key value pairs.</returns>
/// <remarks>
/// This function doesn't support split headers and it will stop reading when it hits two consecutive line breaks.
/// </remarks>
var headers = {};
var parts;
var line;
var pos;
do {
pos = context.position;
line = readLine(text, context);
parts = responseHeaderRegex.exec(line);
if (parts !== null) {
headers[parts[1]] = parts[2];
} else {
// Whatever was found is not a header, so reset the context position.
context.position = pos;
}
} while (line && parts);
normalizeHeaders(headers);
return headers;
};
var readResponse = function (text, context, delimiter) {
/// <summary>
/// Parses an HTTP response.
/// </summary>
/// <param name="text" type="String" optional="false">Text representing the http response.</param>
/// <param name="context" optional="false">Context used for parsing.</param>
/// <param name="delimiter" type="String" optional="false">String used as delimiter of the multipart response parts.</param>
/// <returns>Object representing the http response.</returns>
// Read the status line.
var pos = context.position;
var match = responseStatusRegex.exec(readLine(text, context));
var statusCode;
var statusText;
var headers;
if (match) {
statusCode = match[1];
statusText = match[2];
headers = readHeaders(text, context);
readLine(text, context);
} else {
context.position = pos;
}
return {
statusCode: statusCode,
statusText: statusText,
headers: headers,
body: readTo(text, context, "\r\n" + delimiter)
};
};
var readLine = function (text, context) {
/// <summary>
/// Returns a substring from the position defined by the context up to the next line break (CRLF).
/// </summary>
/// <param name="text" type="String" optional="false">Input string.</param>
/// <param name="context" optional="false">Context used for reading the input string.</param>
/// <returns type="String">Substring to the first ocurrence of a line break or null if none can be found. </returns>
return readTo(text, context, "\r\n");
};
var readTo = function (text, context, str) {
/// <summary>
/// Returns a substring from the position given by the context up to value defined by the str parameter and increments the position in the context.
/// </summary>
/// <param name="text" type="String" optional="false">Input string.</param>
/// <param name="context" type="Object" optional="false">Context used for reading the input string.</param>
/// <param name="str" type="String" optional="true">Substring to read up to.</param>
/// <returns type="String">Substring to the first ocurrence of str or the end of the input string if str is not specified. Null if the marker is not found.</returns>
var start = context.position || 0;
var end = text.length;
if (str) {
end = text.indexOf(str, start);
if (end === -1) {
return null;
}
context.position = end + str.length;
} else {
context.position = end;
}
return text.substring(start, end);
};
var writeBatch = function (data, context) {
/// <summary>
/// Serializes a batch request object to a string.
/// </summary>
/// <param name="data" optional="false">Batch request object in payload representation format</param>
/// <param name="context" optional="false">Context used for the serialization</param>
/// <returns type="String">String representing the batch request</returns>
if (!isBatch(data)) {
throw { message: "Data is not a batch object." };
}
var batchBoundary = createBoundary("batch_");
var batchParts = data.__batchRequests;
var batch = "";
var i, len;
for (i = 0, len = batchParts.length; i < len; i++) {
batch += writeBatchPartDelimiter(batchBoundary, false) +
writeBatchPart(batchParts[i], context);
}
batch += writeBatchPartDelimiter(batchBoundary, true);
// Register the boundary with the request content type.
var contentTypeProperties = context.contentType.properties;
contentTypeProperties.boundary = batchBoundary;
return batch;
};
var writeBatchPartDelimiter = function (boundary, close) {
/// <summary>
/// Creates the delimiter that indicates that start or end of an individual request.
/// </summary>
/// <param name="boundary" type="String" optional="false">Boundary string used to indicate the start of the request</param>
/// <param name="close" type="Boolean">Flag indicating that a close delimiter string should be generated</param>
/// <returns type="String">Delimiter string</returns>
var result = "\r\n--" + boundary;
if (close) {
result += "--";
}
return result + "\r\n";
};
var writeBatchPart = function (part, context, nested) {
/// <summary>
/// Serializes a part of a batch request to a string. A part can be either a GET request or
/// a change set grouping several CUD (create, update, delete) requests.
/// </summary>
/// <param name="part" optional="false">Request or change set object in payload representation format</param>
/// <param name="context" optional="false">Object containing context information used for the serialization</param>
/// <param name="nested" type="boolean" optional="true">Flag indicating that the part is nested inside a change set</param>
/// <returns type="String">String representing the serialized part</returns>
/// <remarks>
/// A change set is an array of request objects and they cannot be nested inside other change sets.
/// </remarks>
var changeSet = part.__changeRequests;
var result;
if (isArray(changeSet)) {
if (nested) {
throw { message: "Not Supported: change set nested in other change set" };
}
var changeSetBoundary = createBoundary("changeset_");
result = "Content-Type: " + batchMediaType + "; boundary=" + changeSetBoundary + "\r\n";
var i, len;
for (i = 0, len = changeSet.length; i < len; i++) {
result += writeBatchPartDelimiter(changeSetBoundary, false) +
writeBatchPart(changeSet[i], context, true);
}
result += writeBatchPartDelimiter(changeSetBoundary, true);
} else {
result = "Content-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\n";
var partContext = extend({}, context);
partContext.handler = handler;
partContext.request = part;
partContext.contentType = null;
prepareRequest(part, partHandler(context), partContext);
result += writeRequest(part);
}
return result;
};
var writeRequest = function (request) {
/// <summary>
/// Serializes a request object to a string.
/// </summary>
/// <param name="request" optional="false">Request object to serialize</param>
/// <returns type="String">String representing the serialized request</returns>
var result = (request.method ? request.method : "GET") + " " + request.requestUri + " HTTP/1.1\r\n";
for (var name in request.headers) {
if (request.headers[name]) {
result = result + name + ": " + request.headers[name] + "\r\n";
}
}
result += "\r\n";
if (request.body) {
result += request.body;
}
return result;
};
odata.batchHandler = handler(batchParser, batchSerializer, batchMediaType, MAX_DATA_SERVICE_VERSION);
// DATAJS INTERNAL START
odata.batchSerializer = batchSerializer;
odata.writeRequest = writeRequest;
// DATAJS INTERNAL END
// CONTENT END
})(this);