blob: a6d850e42f5f4a8142094aacbe0c66e76b06f9ce [file] [log] [blame]
/*
* 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.
*/
/* {
oldname:'odata-batch.js',
updated:'20140514 12:59'
}*/
var utils = require('./../datajs.js').utils;
var odataUtils = require('./utils.js');
var odataHandler = require('./handler.js');
// Imports
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;
// 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.
*/
var hex16 = function () {
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>
*/
var createBoundary = function (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
*/
var partHandler = function (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.
*/
var currentBoundary = function (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.
*/
var batchParser = function (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.#
*/
var batchSerializer = function (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.
*/
var readBatch = function (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;
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>
* @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.
*/
var readHeaders = function (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.
*/
var readResponse = function (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.
*/
var readLine = function (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>
* @param context - Context used for reading the input string.</param>
* @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.
*/
var readTo = function (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
*/
var writeBatch = function (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>
* @param {Boolean} close - Flag indicating that a close delimiter string should be generated
* @returns {String} Delimiter string
*/
var writeBatchPartDelimiter = function (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>
* @param context - Object containing context information used for the serialization</param>
* @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.
*/
var writeBatchPart = function (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</param>
* @returns {String} String representing the serialized request
*/
var writeRequest = function (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;
};
exports.batchHandler = handler(batchParser, batchSerializer, batchMediaType, MAX_DATA_SERVICE_VERSION);
// DATAJS INTERNAL START
exports.batchSerializer = batchSerializer;
exports.writeRequest = writeRequest;
// DATAJS INTERNAL END