lib/odata/batch.js (216 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ 'use strict'; /** @module odata/batch */ var utils = require('./../utils.js'); var odataUtils = require('./odatautils.js'); var odataHandler = require('./handler.js'); var extend = utils.extend; var isArray = utils.isArray; var trimString = utils.trimString; var contentType = odataHandler.contentType; var handler = odataHandler.handler; var isBatch = odataUtils.isBatch; var MAX_DATA_SERVICE_VERSION = odataHandler.MAX_DATA_SERVICE_VERSION; var normalizeHeaders = odataUtils.normalizeHeaders; //TODO var payloadTypeOf = odata.payloadTypeOf; var prepareRequest = odataUtils.prepareRequest; // Imports // CONTENT START var batchMediaType = "multipart/mixed"; var responseStatusRegex = /^HTTP\/1\.\d (\d{3}) (.*)$/i; var responseHeaderRegex = /^([^()<>@,;:\\"\/[\]?={} \t]+)\s?:\s?(.*)/; /** Calculates a random 16 bit number and returns it in hexadecimal format. * @returns {String} A 16-bit number in hex format. */ function hex16() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substr(1); } /** Creates a string that can be used as a multipart request boundary. * @param {String} [prefix] - * @returns {String} Boundary string of the format: <prefix><hex16>-<hex16>-<hex16> */ function createBoundary(prefix) { return prefix + hex16() + "-" + hex16() + "-" + hex16(); } /** Gets the handler for data serialization of individual requests / responses in a batch. * @param context - Context used for data serialization. * @returns Handler object */ function partHandler(context) { return context.handler.partHandler; } /** Gets the current boundary used for parsing the body of a multipart response. * @param context - Context used for parsing a multipart response. * @returns {String} Boundary string. */ function currentBoundary(context) { var boundaries = context.boundaries; return boundaries[boundaries.length - 1]; } /** Parses a batch response. * @param handler - This handler. * @param {String} text - Batch text. * @param {Object} context - Object with parsing context. * @return An object representation of the batch. */ function batchParser(handler, text, context) { var boundary = context.contentType.properties["boundary"]; return { __batchResponses: readBatch(text, { boundaries: [boundary], handlerContext: context }) }; } /** Serializes a batch object representation into text. * @param handler - This handler. * @param {Object} data - Representation of a batch. * @param {Object} context - Object with parsing context. * @return An text representation of the batch object; undefined if not applicable.# */ function batchSerializer(handler, data, context) { var cType = context.contentType = context.contentType || contentType(batchMediaType); if (cType.mediaType === batchMediaType) { return writeBatch(data, context); } } /** Parses a multipart/mixed response body from from the position defined by the context. * @param {String} text - Body of the multipart/mixed response. * @param context - Context used for parsing. * @return Array of objects representing the individual responses. */ function readBatch(text, context) { 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 = null; 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; } /** Parses the http headers in the text from the position defined by the context. * @param {String} text - Text containing an http response's headers * @param context - Context used for parsing. * @returns Object containing the headers as key value pairs. * This function doesn't support split headers and it will stop reading when it hits two consecutive line breaks. */ function readHeaders(text, context) { 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; } /** Parses an HTTP response. * @param {String} text -Text representing the http response. * @param context optional - Context used for parsing. * @param {String} delimiter -String used as delimiter of the multipart response parts. * @return Object representing the http response. */ function readResponse(text, context, delimiter) { // 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) }; } /** Returns a substring from the position defined by the context up to the next line break (CRLF). * @param {String} text - Input string. * @param context - Context used for reading the input string. * @returns {String} Substring to the first ocurrence of a line break or null if none can be found. */ function readLine(text, context) { return readTo(text, context, "\r\n"); } /** 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. * @param {String} text - Input string. * @param context - Context used for reading the input string. * @param {String} [str] - Substring to read up to. * @returns {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. */ function readTo(text, context, str) { 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); } /** Serializes a batch request object to a string. * @param data - Batch request object in payload representation format * @param context - Context used for the serialization * @returns {String} String representing the batch request */ function writeBatch(data, context) { 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; } /** Creates the delimiter that indicates that start or end of an individual request. * @param {String} boundary Boundary string used to indicate the start of the request * @param {Boolean} close - Flag indicating that a close delimiter string should be generated * @returns {String} Delimiter string */ function writeBatchPartDelimiter(boundary, close) { var result = "\r\n--" + boundary; if (close) { result += "--"; } return result + "\r\n"; } /** 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. * @param part - Request or change set object in payload representation format * @param context - Object containing context information used for the serialization * @param {boolean} [nested] - * @returns {String} String representing the serialized part * A change set is an array of request objects and they cannot be nested inside other change sets. */ function writeBatchPart(part, context, nested) { 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; } /** Serializes a request object to a string. * @param request - Request object to serialize * @returns {String} String representing the serialized request */ function writeRequest(request) { 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; } /** batchHandler (see {@link module:odata/batch~batchParser}) */ exports.batchHandler = handler(batchParser, batchSerializer, batchMediaType, MAX_DATA_SERVICE_VERSION); /** batchSerializer (see {@link module:odata/batch~batchSerializer}) */ exports.batchSerializer = batchSerializer; /** writeRequest (see {@link module:odata/batch~writeRequest}) */ exports.writeRequest = writeRequest;