| /// <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.datajs || {}; |
| var odata = window.OData || {}; |
| |
| // Imports |
| |
| var extend = datajs.extend; |
| var isArray = datajs.isArray; |
| var trimString = datajs.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); |